mirror of
https://github.com/matrix-construct/construct
synced 2024-11-12 13:01:07 +01:00
ircd:Ⓜ️ Various init reorg related.
This commit is contained in:
parent
19b4e58944
commit
c442954e12
5 changed files with 210 additions and 215 deletions
|
@ -14,10 +14,15 @@
|
||||||
namespace ircd::m
|
namespace ircd::m
|
||||||
{
|
{
|
||||||
template<class prototype> struct import;
|
template<class prototype> struct import;
|
||||||
|
struct imports extern imports;
|
||||||
extern std::map<std::string, ircd::module, std::less<>> imports;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ircd::m::imports
|
||||||
|
:std::map<std::string, ircd::module, std::less<>>
|
||||||
|
{
|
||||||
|
void init();
|
||||||
|
};
|
||||||
|
|
||||||
template<class prototype>
|
template<class prototype>
|
||||||
struct ircd::m::import
|
struct ircd::m::import
|
||||||
:mods::import<prototype>
|
:mods::import<prototype>
|
||||||
|
|
|
@ -14,21 +14,13 @@
|
||||||
/// Matrix Protocol System
|
/// Matrix Protocol System
|
||||||
namespace ircd::m
|
namespace ircd::m
|
||||||
{
|
{
|
||||||
|
using ircd::hash;
|
||||||
|
|
||||||
struct init;
|
struct init;
|
||||||
|
|
||||||
|
IRCD_OVERLOAD(generate)
|
||||||
|
|
||||||
extern struct log::log log;
|
extern struct log::log log;
|
||||||
extern std::list<ircd::net::listener> listeners;
|
|
||||||
|
|
||||||
using ircd::hash;
|
|
||||||
IRCD_OVERLOAD(generate)
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace ircd::m::self
|
|
||||||
{
|
|
||||||
struct init;
|
|
||||||
|
|
||||||
string_view host();
|
|
||||||
bool host(const string_view &);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace ircd::m::vm
|
namespace ircd::m::vm
|
||||||
|
@ -36,24 +28,10 @@ namespace ircd::m::vm
|
||||||
struct opts;
|
struct opts;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace ircd::m
|
|
||||||
{
|
|
||||||
extern struct user me;
|
|
||||||
extern struct room my_room;
|
|
||||||
extern struct node my_node;
|
|
||||||
|
|
||||||
inline string_view my_host() { return self::host(); }
|
|
||||||
inline bool my_host(const string_view &h) { return self::host(h); }
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace ircd
|
|
||||||
{
|
|
||||||
using m::my_host;
|
|
||||||
}
|
|
||||||
|
|
||||||
#include "name.h"
|
#include "name.h"
|
||||||
#include "error.h"
|
#include "error.h"
|
||||||
#include "import.h"
|
#include "import.h"
|
||||||
|
#include "self.h"
|
||||||
#include "id.h"
|
#include "id.h"
|
||||||
#include "event.h"
|
#include "event.h"
|
||||||
#include "dbs.h"
|
#include "dbs.h"
|
||||||
|
@ -80,29 +58,19 @@ namespace ircd
|
||||||
#include "hook.h"
|
#include "hook.h"
|
||||||
#include "visible.h"
|
#include "visible.h"
|
||||||
|
|
||||||
struct ircd::m::self::init
|
|
||||||
{
|
|
||||||
init(const json::object &config);
|
|
||||||
~init() noexcept;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct ircd::m::init
|
struct ircd::m::init
|
||||||
{
|
{
|
||||||
struct modules;
|
|
||||||
struct listeners;
|
struct listeners;
|
||||||
|
|
||||||
json::object config;
|
|
||||||
self::init _self;
|
self::init _self;
|
||||||
keys::init _keys;
|
|
||||||
dbs::init _dbs;
|
dbs::init _dbs;
|
||||||
std::unique_ptr<modules> modules;
|
keys::init _keys;
|
||||||
std::unique_ptr<listeners> listeners;
|
|
||||||
|
|
||||||
static void bootstrap();
|
static void bootstrap();
|
||||||
|
void init_imports();
|
||||||
void close();
|
void close();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
init();
|
init(const string_view &origin);
|
||||||
~init() noexcept;
|
~init() noexcept;
|
||||||
};
|
};
|
||||||
|
|
52
include/ircd/m/self.h
Normal file
52
include/ircd/m/self.h
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#define HAVE_IRCD_M_SELF_H
|
||||||
|
|
||||||
|
namespace ircd::m::self
|
||||||
|
{
|
||||||
|
struct init;
|
||||||
|
|
||||||
|
string_view host();
|
||||||
|
bool host(const string_view &);
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace ircd::m
|
||||||
|
{
|
||||||
|
extern struct user me;
|
||||||
|
extern struct room my_room;
|
||||||
|
extern struct node my_node;
|
||||||
|
|
||||||
|
string_view my_host();
|
||||||
|
bool my_host(const string_view &);
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace ircd
|
||||||
|
{
|
||||||
|
using m::my_host;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ircd::m::self::init
|
||||||
|
{
|
||||||
|
init(const string_view &origin);
|
||||||
|
};
|
||||||
|
|
||||||
|
inline ircd::string_view
|
||||||
|
ircd::m::my_host()
|
||||||
|
{
|
||||||
|
return self::host();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool
|
||||||
|
ircd::m::my_host(const string_view &host)
|
||||||
|
{
|
||||||
|
return self::host(host);
|
||||||
|
}
|
228
ircd/m/m.cc
228
ircd/m/m.cc
|
@ -14,10 +14,6 @@ ircd::m::log
|
||||||
"matrix", 'm'
|
"matrix", 'm'
|
||||||
};
|
};
|
||||||
|
|
||||||
decltype(ircd::m::listeners)
|
|
||||||
ircd::m::listeners
|
|
||||||
{};
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// init
|
// init
|
||||||
//
|
//
|
||||||
|
@ -36,64 +32,22 @@ me_offline_status_msg
|
||||||
{ "default", "Catch ya on the flip side..." }
|
{ "default", "Catch ya on the flip side..." }
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ircd::m::init::modules
|
|
||||||
{
|
|
||||||
modules(const json::object &);
|
|
||||||
~modules() noexcept;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct ircd::m::init::listeners
|
|
||||||
{
|
|
||||||
listeners(const json::object &);
|
|
||||||
~listeners() noexcept;
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// init::init
|
// init::init
|
||||||
//
|
//
|
||||||
|
|
||||||
ircd::m::init::init()
|
ircd::m::init::init(const string_view &origin)
|
||||||
try
|
try
|
||||||
:config
|
:_self
|
||||||
{
|
{
|
||||||
ircd::conf::config
|
origin
|
||||||
}
|
|
||||||
,_self
|
|
||||||
{
|
|
||||||
this->config
|
|
||||||
}
|
}
|
||||||
,_keys
|
,_keys
|
||||||
{
|
{
|
||||||
this->config
|
{}
|
||||||
}
|
|
||||||
,modules{[this]
|
|
||||||
{
|
|
||||||
auto ret{std::make_unique<struct modules>(config)};
|
|
||||||
|
|
||||||
//TODO: XXX
|
|
||||||
// Because s_conf is loaded before other modules are loaded, all items
|
|
||||||
// have not registered at that time. s_conf reads the !conf room on load
|
|
||||||
// but each conf::item does not read from the conf room itself when it
|
|
||||||
// loads after that. Instead they start with their "default" value. This
|
|
||||||
// isn't good, but for the time being we can trigger a rehash here.
|
|
||||||
import<void ()> rehash_conf
|
|
||||||
{
|
|
||||||
"s_conf", "rehash_conf"
|
|
||||||
};
|
|
||||||
|
|
||||||
// if(rehash_conf)
|
|
||||||
// rehash_conf();
|
|
||||||
|
|
||||||
if(db::sequence(*dbs::events) == 0)
|
|
||||||
bootstrap();
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}()}
|
|
||||||
,listeners
|
|
||||||
{
|
|
||||||
std::make_unique<struct listeners>(config)
|
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
init_imports();
|
||||||
presence::set(me, "online", me_online_status_msg);
|
presence::set(me, "online", me_online_status_msg);
|
||||||
}
|
}
|
||||||
catch(const m::error &e)
|
catch(const m::error &e)
|
||||||
|
@ -114,8 +68,8 @@ catch(const std::exception &e)
|
||||||
ircd::m::init::~init()
|
ircd::m::init::~init()
|
||||||
noexcept try
|
noexcept try
|
||||||
{
|
{
|
||||||
listeners.reset(nullptr);
|
|
||||||
presence::set(me, "offline", me_offline_status_msg);
|
presence::set(me, "offline", me_offline_status_msg);
|
||||||
|
m::imports.clear();
|
||||||
}
|
}
|
||||||
catch(const m::error &e)
|
catch(const m::error &e)
|
||||||
{
|
{
|
||||||
|
@ -126,10 +80,11 @@ catch(const m::error &e)
|
||||||
void
|
void
|
||||||
ircd::m::init::close()
|
ircd::m::init::close()
|
||||||
{
|
{
|
||||||
listeners.reset(nullptr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ircd::m::init::modules::modules(const json::object &config)
|
void
|
||||||
|
ircd::m::init::init_imports()
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if(ircd::noautomod)
|
if(ircd::noautomod)
|
||||||
|
@ -143,119 +98,27 @@ try
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manually load first modules
|
m::imports.init();
|
||||||
m::imports.emplace("vm"s, "vm"s);
|
|
||||||
m::imports.emplace("vm_fetch"s, "vm_fetch"s);
|
|
||||||
|
|
||||||
// The order of these prefixes will be the loading order. Order of
|
if(db::sequence(*dbs::events) == 0)
|
||||||
// specific modules within a prefix is not determined here.
|
bootstrap();
|
||||||
static const string_view prefixes[]
|
|
||||||
{
|
|
||||||
"s_", "m_", "key_", "media_", "client_", "federation_"
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load modules by prefix.
|
|
||||||
for(const auto &prefix : prefixes)
|
|
||||||
for(const auto &name : mods::available())
|
|
||||||
if(startswith(name, prefix))
|
|
||||||
m::imports.emplace(name, name);
|
|
||||||
|
|
||||||
// Manually load last modules
|
|
||||||
m::imports.emplace("webroot"s, "webroot"s);
|
|
||||||
}
|
}
|
||||||
catch(...)
|
catch(...)
|
||||||
{
|
|
||||||
this->~modules();
|
|
||||||
}
|
|
||||||
|
|
||||||
ircd::m::init::modules::~modules()
|
|
||||||
noexcept
|
|
||||||
{
|
{
|
||||||
m::imports.clear();
|
m::imports.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace ircd::m
|
|
||||||
{
|
|
||||||
static void init_listener(const json::object &config, const string_view &name, const json::object &conf);
|
|
||||||
}
|
|
||||||
|
|
||||||
ircd::m::init::listeners::listeners(const json::object &config)
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if(ircd::nolisten)
|
|
||||||
{
|
|
||||||
log::warning
|
|
||||||
{
|
|
||||||
"Not listening on any addresses because nolisten flag is set."
|
|
||||||
};
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const json::array listeners
|
|
||||||
{
|
|
||||||
config[{"ircd", "listen"}]
|
|
||||||
};
|
|
||||||
|
|
||||||
for(const auto &name : listeners) try
|
|
||||||
{
|
|
||||||
const json::object &opts
|
|
||||||
{
|
|
||||||
config.at({"listen", unquote(name)})
|
|
||||||
};
|
|
||||||
|
|
||||||
if(!opts.has("tmp_dh_path"))
|
|
||||||
throw user_error
|
|
||||||
{
|
|
||||||
"Listener %s requires a 'tmp_dh_path' in the config. We do not"
|
|
||||||
" create this yet. Try `openssl dhparam -outform PEM -out dh512.pem 512`",
|
|
||||||
name
|
|
||||||
};
|
|
||||||
|
|
||||||
init_listener(config, unquote(name), opts);
|
|
||||||
}
|
|
||||||
catch(const json::not_found &e)
|
|
||||||
{
|
|
||||||
throw ircd::user_error
|
|
||||||
{
|
|
||||||
"Failed to find configuration block for listener %s", name
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch(...)
|
|
||||||
{
|
|
||||||
this->~listeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
ircd::m::init::listeners::~listeners()
|
|
||||||
noexcept
|
|
||||||
{
|
|
||||||
m::listeners.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
ircd::m::init_listener(const json::object &config,
|
|
||||||
const string_view &name,
|
|
||||||
const json::object &opts)
|
|
||||||
{
|
|
||||||
m::listeners.emplace_back(name, opts, []
|
|
||||||
(const auto &sock)
|
|
||||||
{
|
|
||||||
add_client(sock);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
ircd::m::init::bootstrap()
|
ircd::m::init::bootstrap()
|
||||||
{
|
{
|
||||||
assert(dbs::events);
|
assert(dbs::events);
|
||||||
assert(db::sequence(*dbs::events) == 0);
|
assert(db::sequence(*dbs::events) == 0);
|
||||||
|
|
||||||
ircd::log::notice
|
log::notice
|
||||||
(
|
{
|
||||||
"This appears to be your first time running IRCd because the events "
|
log, "This appears to be your first time running IRCd because the events "
|
||||||
"database is empty. I will be bootstrapping it with initial events now..."
|
"database is empty. I will be bootstrapping it with initial events now..."
|
||||||
);
|
};
|
||||||
|
|
||||||
if(me.user_id.hostname() == "localhost")
|
if(me.user_id.hostname() == "localhost")
|
||||||
log::warning
|
log::warning
|
||||||
|
@ -306,6 +169,11 @@ ircd::m::init::bootstrap()
|
||||||
{
|
{
|
||||||
{ "name", "User Tokens" }
|
{ "name", "User Tokens" }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
log::info
|
||||||
|
{
|
||||||
|
log, "Bootstrap event generation completed nominally."
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -382,43 +250,33 @@ extern ircd::m::room::id::buf users_room_id;
|
||||||
extern ircd::m::room::id::buf tokens_room_id;
|
extern ircd::m::room::id::buf tokens_room_id;
|
||||||
extern ircd::m::room::id::buf nodes_room_id;
|
extern ircd::m::room::id::buf nodes_room_id;
|
||||||
|
|
||||||
ircd::m::self::init::init(const json::object &config)
|
ircd::m::self::init::init(const string_view &origin)
|
||||||
{
|
{
|
||||||
const string_view &origin_name
|
ircd_user_id = {"ircd", origin};
|
||||||
{
|
|
||||||
unquote(config.get({"ircd", "origin"}, "localhost"))
|
|
||||||
};
|
|
||||||
|
|
||||||
ircd_user_id = {"ircd", origin_name};
|
|
||||||
m::me = {ircd_user_id};
|
m::me = {ircd_user_id};
|
||||||
|
|
||||||
ircd_room_id = {"ircd", origin_name};
|
ircd_room_id = {"ircd", origin};
|
||||||
m::my_room = {ircd_room_id};
|
m::my_room = {ircd_room_id};
|
||||||
|
|
||||||
ircd_node_id = {"", origin_name};
|
ircd_node_id = {"", origin};
|
||||||
m::my_node = {ircd_node_id};
|
m::my_node = {ircd_node_id};
|
||||||
|
|
||||||
users_room_id = {"users", origin_name};
|
users_room_id = {"users", origin};
|
||||||
m::user::users = {users_room_id};
|
m::user::users = {users_room_id};
|
||||||
|
|
||||||
tokens_room_id = {"tokens", origin_name};
|
tokens_room_id = {"tokens", origin};
|
||||||
m::user::tokens = {tokens_room_id};
|
m::user::tokens = {tokens_room_id};
|
||||||
|
|
||||||
nodes_room_id = {"nodes", origin_name};
|
nodes_room_id = {"nodes", origin};
|
||||||
m::nodes = {nodes_room_id};
|
m::nodes = {nodes_room_id};
|
||||||
|
|
||||||
if(origin_name == "localhost")
|
if(origin == "localhost")
|
||||||
log::warning
|
log::warning
|
||||||
{
|
{
|
||||||
"The ircd.origin is configured or has defaulted to 'localhost'"
|
"The origin is configured or has defaulted to 'localhost'"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ircd::m::self::init::~init()
|
|
||||||
noexcept
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// m/vm.h
|
// m/vm.h
|
||||||
|
@ -797,7 +655,7 @@ ircd::m::keys::init::certificate()
|
||||||
{
|
{
|
||||||
const std::string origin
|
const std::string origin
|
||||||
{
|
{
|
||||||
unquote(this->config.get({"ircd", "origin"}, "localhost"s))
|
m::self::host()
|
||||||
};
|
};
|
||||||
|
|
||||||
const json::object config
|
const json::object config
|
||||||
|
@ -3602,6 +3460,30 @@ decltype(ircd::m::imports)
|
||||||
ircd::m::imports
|
ircd::m::imports
|
||||||
{};
|
{};
|
||||||
|
|
||||||
|
void
|
||||||
|
ircd::m::imports::init()
|
||||||
|
{
|
||||||
|
// Manually load first modules
|
||||||
|
this->emplace("vm"s, "vm"s);
|
||||||
|
this->emplace("vm_fetch"s, "vm_fetch"s);
|
||||||
|
|
||||||
|
// The order of these prefixes will be the loading order. Order of
|
||||||
|
// specific modules within a prefix is not determined here.
|
||||||
|
static const string_view prefixes[]
|
||||||
|
{
|
||||||
|
"s_", "m_", "key_", "media_", "client_", "federation_"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load modules by prefix.
|
||||||
|
for(const auto &prefix : prefixes)
|
||||||
|
for(const auto &name : mods::available())
|
||||||
|
if(startswith(name, prefix))
|
||||||
|
this->emplace(name, name);
|
||||||
|
|
||||||
|
// Manually load last modules
|
||||||
|
this->emplace("webroot"s, "webroot"s);
|
||||||
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// m/error.h
|
// m/error.h
|
||||||
|
|
88
modules/s_listen.cc
Normal file
88
modules/s_listen.cc
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
using namespace ircd;
|
||||||
|
|
||||||
|
mapi::header
|
||||||
|
IRCD_MODULE
|
||||||
|
{
|
||||||
|
"Server listeners"
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace ircd::m
|
||||||
|
{
|
||||||
|
void init_listener(const json::object &config);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
create_listener(const m::event &)
|
||||||
|
{
|
||||||
|
std::cout << "hi" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
const m::hookfn<>
|
||||||
|
create_listener_hook
|
||||||
|
{
|
||||||
|
create_listener,
|
||||||
|
{
|
||||||
|
{ "_site", "vm.notify" },
|
||||||
|
{ "room_id", "!ircd" },
|
||||||
|
{ "type", "m.room.create" },
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void
|
||||||
|
ircd::m::init_listener(const json::object &config)
|
||||||
|
{
|
||||||
|
if(ircd::nolisten)
|
||||||
|
{
|
||||||
|
log::warning
|
||||||
|
{
|
||||||
|
"Not listening on any addresses because nolisten flag is set."
|
||||||
|
};
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const json::array listeners
|
||||||
|
{
|
||||||
|
config[{"ircd", "listen"}]
|
||||||
|
};
|
||||||
|
|
||||||
|
for(const auto &name : listeners) try
|
||||||
|
{
|
||||||
|
const json::object &opts
|
||||||
|
{
|
||||||
|
config.at({"listen", unquote(name)})
|
||||||
|
};
|
||||||
|
|
||||||
|
if(!opts.has("tmp_dh_path"))
|
||||||
|
throw user_error
|
||||||
|
{
|
||||||
|
"Listener %s requires a 'tmp_dh_path' in the config. We do not"
|
||||||
|
" create this yet. Try `openssl dhparam -outform PEM -out dh512.pem 512`",
|
||||||
|
name
|
||||||
|
};
|
||||||
|
/*
|
||||||
|
m::listeners.emplace_back(unquote(name), opts, []
|
||||||
|
(const auto &sock)
|
||||||
|
{
|
||||||
|
add_client(sock);
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
catch(const json::not_found &e)
|
||||||
|
{
|
||||||
|
throw ircd::user_error
|
||||||
|
{
|
||||||
|
"Failed to find configuration block for listener %s", name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue