mirror of
https://github.com/qicosmos/rest_rpc.git
synced 2026-08-29 08:34:47 +08:00
string_view no copy and no serialize
This commit is contained in:
@@ -6,8 +6,10 @@
|
||||
#include "logger.hpp"
|
||||
#include "meta_util.hpp"
|
||||
#include "rest_rpc_protocol.hpp"
|
||||
#include "string_resize.hpp"
|
||||
#include "traits.h"
|
||||
#include "use_asio.hpp"
|
||||
#include "util.hpp"
|
||||
#include <asio/experimental/awaitable_operators.hpp>
|
||||
#include <asio/steady_timer.hpp>
|
||||
using namespace asio::experimental::awaitable_operators;
|
||||
@@ -18,6 +20,10 @@ template <typename R> struct call_result {
|
||||
R value;
|
||||
};
|
||||
|
||||
template <> struct call_result<void> {
|
||||
rpc_errc ec;
|
||||
};
|
||||
|
||||
class client {
|
||||
public:
|
||||
client() : socket_(get_global_executor()) {}
|
||||
@@ -121,7 +127,9 @@ private:
|
||||
std::vector<asio::const_buffer> buffers;
|
||||
buffers.reserve(2);
|
||||
buffers.push_back(asio::buffer(&header, sizeof(rest_rpc_header)));
|
||||
buffers.push_back(asio::buffer(buf.data(), buf.size()));
|
||||
if constexpr (sizeof...(Args) > 0) {
|
||||
buffers.push_back(asio::buffer(buf.data(), buf.size()));
|
||||
}
|
||||
|
||||
using R = typename function_traits<decltype(func)>::return_type;
|
||||
call_result<R> result{};
|
||||
@@ -150,14 +158,19 @@ private:
|
||||
|
||||
detail::resize(body_, resp_header.body_len);
|
||||
std::tie(ec, size) = co_await asio::async_read(
|
||||
socket_, asio::buffer(body_), asio::as_tuple(asio::use_awaitable));
|
||||
socket_, asio::buffer(body_.data(), body_.size()),
|
||||
asio::as_tuple(asio::use_awaitable));
|
||||
if (ec) {
|
||||
REST_LOG_WARNING << "read body error: " << ec.message();
|
||||
result.ec = rpc_errc::read_error;
|
||||
co_return result;
|
||||
}
|
||||
result.ec = (rpc_errc)body_[0];
|
||||
result.value = codec.unpack<R>(body_.data() + 1, resp_header.body_len - 1);
|
||||
if constexpr (!std::is_void_v<R>) {
|
||||
result.value = codec.unpack<R>(
|
||||
std::string_view(body_.data() + 1, resp_header.body_len - 1));
|
||||
}
|
||||
|
||||
co_return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,36 @@
|
||||
#ifndef REST_RPC_CODEC_H_
|
||||
#define REST_RPC_CODEC_H_
|
||||
|
||||
#include "traits.h"
|
||||
#include <charconv>
|
||||
#include <msgpack.hpp>
|
||||
|
||||
namespace rest_rpc {
|
||||
namespace rpc_service {
|
||||
|
||||
template <typename Arg> auto pack_one(Arg &&arg) {
|
||||
if constexpr (util::CharArrayRef<Arg> || util::CharArray<Arg> ||
|
||||
util::string<Arg>) {
|
||||
return std::string_view(std::forward<Arg>(arg));
|
||||
} else {
|
||||
return std::to_string(arg);
|
||||
}
|
||||
}
|
||||
|
||||
using buffer_type = msgpack::sbuffer;
|
||||
struct msgpack_codec {
|
||||
const static size_t init_size = 2 * 1024;
|
||||
|
||||
template <typename... Args> static buffer_type pack_args(Args &&...args) {
|
||||
buffer_type buffer(init_size);
|
||||
msgpack::pack(buffer, std::forward_as_tuple(std::forward<Args>(args)...));
|
||||
return buffer;
|
||||
template <typename... Args> static auto pack_args(Args &&...args) {
|
||||
if constexpr (sizeof...(Args) == 0) {
|
||||
return std::string_view{};
|
||||
} else if constexpr (sizeof...(Args) == 1 && util::is_basic_v<Args...>) {
|
||||
return pack_one(std::forward<Args>(args)...);
|
||||
} else {
|
||||
buffer_type buffer(init_size);
|
||||
msgpack::pack(buffer, std::forward_as_tuple(std::forward<Args>(args)...));
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Arg> static std::string pack_to_string(Arg &arg) {
|
||||
@@ -46,6 +63,23 @@ struct msgpack_codec {
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T> T unpack(std::string_view data) {
|
||||
if constexpr (std::is_fundamental_v<T>) {
|
||||
T t;
|
||||
auto r = std::from_chars(data.data(), data.data() + data.size(), t);
|
||||
if (r.ec != std::errc()) {
|
||||
throw std::invalid_argument("unpack failed: Args not match!");
|
||||
}
|
||||
return t;
|
||||
} else if constexpr (std::is_same_v<std::string, T>) {
|
||||
return std::string(data);
|
||||
} else if constexpr (std::is_same_v<std::string_view, T>) {
|
||||
return data;
|
||||
} else {
|
||||
return unpack<T>(data.data(), data.size());
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
msgpack::unpacked msg_;
|
||||
};
|
||||
|
||||
@@ -79,9 +79,10 @@ auto apply_helper(F &&f, Tuple &&tp, nonstd::index_sequence<Idx...>)
|
||||
}
|
||||
|
||||
template <typename F, typename Tuple>
|
||||
auto apply(F &&f, Tuple &&tp) -> decltype(apply_helper(
|
||||
std::forward<F>(f), std::forward<Tuple>(tp),
|
||||
make_index_sequence<std::tuple_size<decay_t<Tuple>>::value>{})) {
|
||||
auto apply(F &&f, Tuple &&tp)
|
||||
-> decltype(apply_helper(
|
||||
std::forward<F>(f), std::forward<Tuple>(tp),
|
||||
make_index_sequence<std::tuple_size<decay_t<Tuple>>::value>{})) {
|
||||
return apply_helper(
|
||||
std::forward<F>(f), std::forward<Tuple>(tp),
|
||||
make_index_sequence<std::tuple_size<decay_t<Tuple>>::value>{});
|
||||
|
||||
@@ -28,7 +28,7 @@ struct function_traits<Ret(Arg, Args...)> {
|
||||
public:
|
||||
enum { arity = sizeof...(Args) + 1 };
|
||||
typedef Ret function_type(Arg, Args...);
|
||||
typedef Ret return_type;
|
||||
typedef std::remove_cvref_t<Ret> return_type;
|
||||
using stl_function_type = std::function<function_type>;
|
||||
typedef Ret (*pointer)(Arg, Args...);
|
||||
|
||||
@@ -51,7 +51,7 @@ template <typename Ret> struct function_traits<Ret()> {
|
||||
public:
|
||||
enum { arity = 0 };
|
||||
typedef Ret function_type();
|
||||
typedef Ret return_type;
|
||||
typedef std::remove_cvref_t<Ret> return_type;
|
||||
using stl_function_type = std::function<function_type>;
|
||||
typedef Ret (*pointer)();
|
||||
|
||||
@@ -123,7 +123,9 @@ using nth_type_of = nonstd::tuple_element_t<N, std::tuple<Args...>>;
|
||||
template <typename... Args>
|
||||
using last_type_of = nth_type_of<sizeof...(Args) - 1, Args...>;
|
||||
|
||||
template <typename T> struct remove_first { using type = T; };
|
||||
template <typename T> struct remove_first {
|
||||
using type = T;
|
||||
};
|
||||
|
||||
template <class First, class... Second>
|
||||
struct remove_first<std::tuple<First, Second...>> {
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
#define any_HAVE_TWEAK_HEADER 1
|
||||
#else
|
||||
#define any_HAVE_TWEAK_HEADER 0
|
||||
//# pragma message("any.hpp: Note: Tweak header not supported.")
|
||||
// # pragma message("any.hpp: Note: Tweak header not supported.")
|
||||
#endif
|
||||
|
||||
// any selection and configuration:
|
||||
@@ -251,7 +251,7 @@ using std::swap;
|
||||
#endif
|
||||
|
||||
// half-open range [lo..hi):
|
||||
//#define any_BETWEEN( v, lo, hi ) ( (lo) <= (v) && (v) < (hi) )
|
||||
// #define any_BETWEEN( v, lo, hi ) ( (lo) <= (v) && (v) < (hi) )
|
||||
|
||||
// Presence of language and library features:
|
||||
|
||||
@@ -387,7 +387,9 @@ using std::tr1::add_const;
|
||||
|
||||
#else
|
||||
|
||||
template <class T> struct add_const { typedef const T type; };
|
||||
template <class T> struct add_const {
|
||||
typedef const T type;
|
||||
};
|
||||
|
||||
#endif // any_HAVE_ADD_CONST
|
||||
|
||||
@@ -401,8 +403,12 @@ using std::tr1::remove_reference;
|
||||
|
||||
#else
|
||||
|
||||
template <class T> struct remove_reference { typedef T type; };
|
||||
template <class T> struct remove_reference<T &> { typedef T type; };
|
||||
template <class T> struct remove_reference {
|
||||
typedef T type;
|
||||
};
|
||||
template <class T> struct remove_reference<T &> {
|
||||
typedef T type;
|
||||
};
|
||||
|
||||
#endif // any_HAVE_REMOVE_REFERENCE
|
||||
|
||||
|
||||
@@ -6,6 +6,14 @@
|
||||
#include <arpa/inet.h>
|
||||
#endif
|
||||
|
||||
inline uint64_t htonll(uint64_t value) {
|
||||
return ((uint64_t)htonl(value & 0xFFFFFFFF) << 32) | htonl(value >> 32);
|
||||
}
|
||||
|
||||
inline uint64_t ntohll(uint64_t value) {
|
||||
return ((uint64_t)ntohl(value & 0xFFFFFFFF) << 32) | ntohl(value >> 32);
|
||||
}
|
||||
|
||||
namespace rest_rpc {
|
||||
inline constexpr uint8_t REST_MAGIC_NUM = 39;
|
||||
struct rest_rpc_header {
|
||||
|
||||
@@ -32,24 +32,29 @@ public:
|
||||
}
|
||||
|
||||
detail::resize(body_, header.body_len);
|
||||
std::tie(ec, size) = co_await asio::async_read(
|
||||
socket_, asio::buffer(body_), asio::as_tuple(asio::use_awaitable));
|
||||
if (ec) {
|
||||
REST_LOG_WARNING << "read body error: " << ec.message();
|
||||
break;
|
||||
|
||||
if (header.body_len > 0) {
|
||||
std::tie(ec, size) = co_await asio::async_read(
|
||||
socket_, asio::buffer(body_), asio::as_tuple(asio::use_awaitable));
|
||||
if (ec) {
|
||||
REST_LOG_WARNING << "read body error: " << ec.message();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// route
|
||||
auto result = router_.route(header.function_id, body_);
|
||||
rest_rpc_header resp_header{};
|
||||
resp_header.magic = 39;
|
||||
resp_header.body_len = result.result.size() + 1;
|
||||
resp_header.body_len = result.size() + 1;
|
||||
prepare_for_send(resp_header);
|
||||
std::vector<asio::const_buffer> buffers;
|
||||
buffers.reserve(3);
|
||||
buffers.push_back(asio::buffer(&resp_header, sizeof(rest_rpc_header)));
|
||||
buffers.push_back(asio::buffer(&result.ec, 1));
|
||||
buffers.push_back(asio::buffer(result.result));
|
||||
if (!result.empty())
|
||||
buffers.push_back(asio::buffer(result.data()));
|
||||
|
||||
std::tie(ec, size) = co_await asio::async_write(
|
||||
socket_, buffers, asio::as_tuple(asio::use_awaitable));
|
||||
if (ec) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#pragma once
|
||||
#include "codec.h"
|
||||
#include "error_code.h"
|
||||
#include "function_name.h"
|
||||
#include "md5.hpp"
|
||||
|
||||
#include "meta_util.hpp"
|
||||
#include "util.hpp"
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
@@ -13,13 +13,12 @@ namespace rest_rpc {
|
||||
struct rpc_result {
|
||||
rpc_errc ec = rpc_errc::ok;
|
||||
std::string result;
|
||||
};
|
||||
std::string_view view;
|
||||
bool empty() { return result.empty() && view.empty(); }
|
||||
size_t size() { return result.empty() ? view.size() : result.size(); }
|
||||
|
||||
template <auto func> constexpr uint32_t get_key() {
|
||||
constexpr auto name = get_func_name<func>();
|
||||
constexpr uint32_t key = MD5::MD5Hash32(name.data(), name.length());
|
||||
return key;
|
||||
}
|
||||
std::string_view data() { return result.empty() ? view : result; }
|
||||
};
|
||||
|
||||
class rpc_router {
|
||||
public:
|
||||
@@ -57,30 +56,28 @@ public:
|
||||
|
||||
rpc_result route(uint32_t key, std::string_view data) {
|
||||
rpc_result route_result{};
|
||||
std::string result;
|
||||
try {
|
||||
rpc_service::msgpack_codec codec;
|
||||
auto it = map_invokers_.find(key);
|
||||
if (it == map_invokers_.end()) {
|
||||
result = "unknown function: " + get_name_by_key(key);
|
||||
route_result.result = "unknown function: " + get_name_by_key(key);
|
||||
route_result.ec = rpc_errc::no_such_function;
|
||||
} else {
|
||||
it->second(data, route_result.ec, result);
|
||||
it->second(data, route_result);
|
||||
route_result.ec = rpc_errc::ok;
|
||||
}
|
||||
} catch (const std::exception &ex) {
|
||||
rpc_service::msgpack_codec codec;
|
||||
result = std::string("exception occur when call").append(ex.what());
|
||||
route_result.result =
|
||||
std::string("exception occur when call").append(ex.what());
|
||||
route_result.ec = rpc_errc::function_exception;
|
||||
} catch (...) {
|
||||
rpc_service::msgpack_codec codec;
|
||||
result = std::string("unknown exception occur when call ")
|
||||
.append(get_name_by_key(key));
|
||||
route_result.result = std::string("unknown exception occur when call ")
|
||||
.append(get_name_by_key(key));
|
||||
route_result.ec = rpc_errc::function_unknown_exception;
|
||||
}
|
||||
|
||||
route_result.result = std::move(result);
|
||||
|
||||
return route_result;
|
||||
}
|
||||
|
||||
@@ -103,65 +100,116 @@ private:
|
||||
template <typename Function>
|
||||
void register_nonmember_func(uint32_t key, Function f) {
|
||||
this->map_invokers_[key] = [f = std::move(f)](std::string_view str,
|
||||
rpc_errc &ec,
|
||||
std::string &result) mutable {
|
||||
rpc_result &ret) mutable {
|
||||
using args_tuple = typename function_traits<Function>::tuple_type;
|
||||
using R = typename function_traits<Function>::return_type;
|
||||
rpc_service::msgpack_codec codec;
|
||||
|
||||
try {
|
||||
auto tp = codec.unpack<args_tuple>(str.data(), str.size());
|
||||
if constexpr (std::is_void_v<R>) {
|
||||
std::apply(f, tp);
|
||||
if constexpr (std::tuple_size_v<args_tuple> == 0) {
|
||||
if constexpr (std::is_void_v<R>) {
|
||||
f();
|
||||
} else {
|
||||
auto r = f();
|
||||
ret.result = rpc_service::msgpack_codec::pack_to_string(r);
|
||||
}
|
||||
} else {
|
||||
auto r = std::apply(f, tp);
|
||||
result = rpc_service::msgpack_codec::pack_to_string(r);
|
||||
rpc_service::msgpack_codec codec;
|
||||
using first_t = std::tuple_element_t<0, args_tuple>;
|
||||
if constexpr (std::tuple_size_v<args_tuple> == 1 &&
|
||||
util::is_basic_v<first_t>) {
|
||||
if constexpr (std::is_void_v<R>) {
|
||||
f(codec.unpack<first_t>(str));
|
||||
} else {
|
||||
if constexpr (std::is_same_v<std::string_view, R>) {
|
||||
ret.view = rpc_service::msgpack_codec::pack_args(
|
||||
f(codec.unpack<first_t>(str)));
|
||||
} else {
|
||||
ret.result = rpc_service::msgpack_codec::pack_args(
|
||||
f(codec.unpack<first_t>(str)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
auto tp = codec.unpack<args_tuple>(str);
|
||||
if constexpr (std::is_void_v<R>) {
|
||||
std::apply(f, tp);
|
||||
} else {
|
||||
auto r = std::apply(f, tp);
|
||||
ret.result = rpc_service::msgpack_codec::pack_to_string(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (std::invalid_argument &e) {
|
||||
ec = rpc_errc::invalid_argument;
|
||||
result = e.what();
|
||||
ret.ec = rpc_errc::invalid_argument;
|
||||
ret.result = e.what();
|
||||
} catch (const std::exception &e) {
|
||||
ec = rpc_errc::function_exception;
|
||||
result = e.what();
|
||||
ret.ec = rpc_errc::function_exception;
|
||||
ret.result = e.what();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
template <typename Function, typename Self>
|
||||
void register_member_func(uint32_t key, const Function &f, Self *self) {
|
||||
this->map_invokers_[key] = [f, self](std::string_view str, rpc_errc &ec,
|
||||
std::string &result) {
|
||||
this->map_invokers_[key] = [f, self](std::string_view str,
|
||||
rpc_result &ret) {
|
||||
using args_tuple = typename function_traits<Function>::tuple_type;
|
||||
using R = typename function_traits<Function>::return_type;
|
||||
rpc_service::msgpack_codec codec;
|
||||
try {
|
||||
auto tp = codec.unpack<args_tuple>(str.data(), str.size());
|
||||
|
||||
if constexpr (std::is_void_v<R>) {
|
||||
std::apply(
|
||||
[self, &f](auto &&...args) {
|
||||
return (*self.*f)(std::forward<decltype(args)>(args)...);
|
||||
},
|
||||
tp);
|
||||
if constexpr (std::tuple_size_v<args_tuple> == 0) {
|
||||
if constexpr (std::is_void_v<R>) {
|
||||
(*self.*f)();
|
||||
} else {
|
||||
auto r = (*self.*f)();
|
||||
ret.result = rpc_service::msgpack_codec::pack_to_string(r);
|
||||
}
|
||||
} else {
|
||||
auto r = std::apply(
|
||||
[self, &f](auto &&...args) {
|
||||
return (*self.*f)(std::forward<decltype(args)>(args)...);
|
||||
},
|
||||
tp);
|
||||
result = rpc_service::msgpack_codec::pack_to_string(r);
|
||||
using first_t = std::tuple_element_t<0, args_tuple>;
|
||||
if constexpr (std::tuple_size_v<args_tuple> == 1 &&
|
||||
util::is_basic_v<first_t>) {
|
||||
if constexpr (std::is_void_v<R>) {
|
||||
(*self.*f)(codec.unpack<first_t>(str));
|
||||
} else {
|
||||
if constexpr (std::is_same_v<std::string_view, R>) {
|
||||
ret.view = rpc_service::msgpack_codec::pack_args(
|
||||
(*self.*f)(codec.unpack<first_t>(str)));
|
||||
} else {
|
||||
ret.result = rpc_service::msgpack_codec::pack_args(
|
||||
(*self.*f)(codec.unpack<first_t>(str)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
auto tp = codec.unpack<args_tuple>(str);
|
||||
|
||||
if constexpr (std::is_void_v<R>) {
|
||||
std::apply(
|
||||
[self, &f](auto &&...args) {
|
||||
return (*self.*f)(std::forward<decltype(args)>(args)...);
|
||||
},
|
||||
tp);
|
||||
} else {
|
||||
auto r = std::apply(
|
||||
[self, &f](auto &&...args) {
|
||||
return (*self.*f)(std::forward<decltype(args)>(args)...);
|
||||
},
|
||||
tp);
|
||||
ret.result = rpc_service::msgpack_codec::pack_to_string(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (std::invalid_argument &e) {
|
||||
ec = rpc_errc::invalid_argument;
|
||||
result = e.what();
|
||||
ret.ec = rpc_errc::invalid_argument;
|
||||
ret.result = e.what();
|
||||
} catch (const std::exception &e) {
|
||||
ec = rpc_errc::function_exception;
|
||||
result = e.what();
|
||||
ret.ec = rpc_errc::function_exception;
|
||||
ret.result = e.what();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
std::unordered_map<uint32_t, std::function<void(std::string_view, rpc_errc &,
|
||||
std::string &)>>
|
||||
std::unordered_map<uint32_t,
|
||||
std::function<void(std::string_view, rpc_result &)>>
|
||||
map_invokers_;
|
||||
std::unordered_map<uint32_t, std::string> key2func_name_;
|
||||
};
|
||||
|
||||
@@ -145,7 +145,9 @@ struct is_invocable
|
||||
template <typename F, typename... Args>
|
||||
inline constexpr bool is_invocable_v = is_invocable<F, Args...>::value;
|
||||
|
||||
template <typename T> struct remove_first { using type = T; };
|
||||
template <typename T> struct remove_first {
|
||||
using type = T;
|
||||
};
|
||||
|
||||
template <class First, class... Second>
|
||||
struct remove_first<std::tuple<First, Second...>> {
|
||||
@@ -171,4 +173,44 @@ struct is_specialization<Ref<Args...>, Ref> : std::true_type {};
|
||||
|
||||
template <typename Test, template <typename...> class Ref>
|
||||
inline constexpr bool is_specialization_v = is_specialization<Test, Ref>::value;
|
||||
|
||||
template <typename Type>
|
||||
concept container = requires(Type container) {
|
||||
typename std::remove_cvref_t<Type>::value_type;
|
||||
container.size();
|
||||
container.begin();
|
||||
container.end();
|
||||
};
|
||||
|
||||
template <typename Type>
|
||||
constexpr bool is_char_t =
|
||||
std::is_same_v<Type, signed char> || std::is_same_v<Type, char> ||
|
||||
std::is_same_v<Type, unsigned char> || std::is_same_v<Type, wchar_t> ||
|
||||
std::is_same_v<Type, char16_t> || std::is_same_v<Type, char32_t>
|
||||
#ifdef __cpp_lib_char8_t
|
||||
|| std::is_same_v<Type, char8_t>
|
||||
#endif
|
||||
;
|
||||
|
||||
template <typename Type>
|
||||
concept string = container<Type> && requires(Type container) {
|
||||
requires is_char_t<typename std::remove_cvref_t<Type>::value_type>;
|
||||
container.length();
|
||||
container.data();
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept CharArrayRef = requires {
|
||||
requires std::is_array_v<std::remove_reference_t<T>> &&
|
||||
std::same_as<std::remove_extent_t<std::remove_cvref_t<T>>, char>;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept CharArray =
|
||||
std::is_array_v<std::remove_reference_t<T>> &&
|
||||
std::same_as<std::remove_extent_t<std::remove_cvref_t<T>>, char>;
|
||||
|
||||
template <typename T, typename...>
|
||||
inline constexpr bool is_basic_v =
|
||||
std::is_fundamental_v<T> || string<T> || CharArray<T> || CharArrayRef<T>;
|
||||
} // namespace rest_rpc::util
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
#include "function_name.h"
|
||||
#include "md5.hpp"
|
||||
|
||||
namespace rest_rpc {
|
||||
template <auto func> constexpr uint32_t get_key() {
|
||||
constexpr auto name = get_func_name<func>();
|
||||
constexpr uint32_t key = MD5::MD5Hash32(name.data(), name.length());
|
||||
return key;
|
||||
}
|
||||
} // namespace rest_rpc
|
||||
Reference in New Issue
Block a user