mirror of
https://github.com/jkcoxson/idevice.git
synced 2026-03-02 06:26:15 +01:00
add diag relay c++ bindings, screenshotr ffi (#58)
This commit is contained in:
60
cpp/include/idevice++/diagnostics_relay.hpp
Normal file
60
cpp/include/idevice++/diagnostics_relay.hpp
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// Jackson Coxson
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <idevice++/bindings.hpp>
|
||||||
|
#include <idevice++/ffi.hpp>
|
||||||
|
#include <idevice++/provider.hpp>
|
||||||
|
#include <idevice++/result.hpp>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace IdeviceFFI {
|
||||||
|
|
||||||
|
using DiagnosticsRelayPtr =
|
||||||
|
std::unique_ptr<DiagnosticsRelayClientHandle,
|
||||||
|
FnDeleter<DiagnosticsRelayClientHandle, diagnostics_relay_client_free>>;
|
||||||
|
|
||||||
|
class DiagnosticsRelay {
|
||||||
|
public:
|
||||||
|
// Factory: connect via Provider
|
||||||
|
static Result<DiagnosticsRelay, FfiError> connect(Provider& provider);
|
||||||
|
|
||||||
|
// Factory: wrap an existing Idevice socket (consumes it on success)
|
||||||
|
static Result<DiagnosticsRelay, FfiError> from_socket(Idevice&& socket);
|
||||||
|
|
||||||
|
// API Methods - queries returning optional plist
|
||||||
|
Result<Option<plist_t>, FfiError> ioregistry(Option<std::string> current_plane,
|
||||||
|
Option<std::string> entry_name,
|
||||||
|
Option<std::string> entry_class) const;
|
||||||
|
|
||||||
|
Result<Option<plist_t>, FfiError> mobilegestalt(Option<std::vector<char*>> keys) const;
|
||||||
|
|
||||||
|
Result<Option<plist_t>, FfiError> gasguage() const;
|
||||||
|
Result<Option<plist_t>, FfiError> nand() const;
|
||||||
|
Result<Option<plist_t>, FfiError> all() const;
|
||||||
|
Result<Option<plist_t>, FfiError> wifi() const;
|
||||||
|
|
||||||
|
// API Methods - actions
|
||||||
|
Result<void, FfiError> restart();
|
||||||
|
Result<void, FfiError> shutdown();
|
||||||
|
Result<void, FfiError> sleep();
|
||||||
|
Result<void, FfiError> goodbye();
|
||||||
|
|
||||||
|
// RAII / moves
|
||||||
|
~DiagnosticsRelay() noexcept = default;
|
||||||
|
DiagnosticsRelay(DiagnosticsRelay&&) noexcept = default;
|
||||||
|
DiagnosticsRelay& operator=(DiagnosticsRelay&&) noexcept = default;
|
||||||
|
DiagnosticsRelay(const DiagnosticsRelay&) = delete;
|
||||||
|
DiagnosticsRelay& operator=(const DiagnosticsRelay&) = delete;
|
||||||
|
|
||||||
|
DiagnosticsRelayClientHandle* raw() const noexcept { return handle_.get(); }
|
||||||
|
static DiagnosticsRelay adopt(DiagnosticsRelayClientHandle* h) noexcept {
|
||||||
|
return DiagnosticsRelay(h);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit DiagnosticsRelay(DiagnosticsRelayClientHandle* h) noexcept : handle_(h) {}
|
||||||
|
DiagnosticsRelayPtr handle_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace IdeviceFFI
|
||||||
159
cpp/src/diagnostics_relay.cpp
Normal file
159
cpp/src/diagnostics_relay.cpp
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
// Jackson Coxson
|
||||||
|
|
||||||
|
#include <idevice++/bindings.hpp>
|
||||||
|
#include <idevice++/diagnostics_relay.hpp>
|
||||||
|
#include <idevice++/ffi.hpp>
|
||||||
|
#include <idevice++/provider.hpp>
|
||||||
|
|
||||||
|
namespace IdeviceFFI {
|
||||||
|
|
||||||
|
// -------- Factory Methods --------
|
||||||
|
|
||||||
|
Result<DiagnosticsRelay, FfiError> DiagnosticsRelay::connect(Provider& provider) {
|
||||||
|
DiagnosticsRelayClientHandle* out = nullptr;
|
||||||
|
FfiError e(::diagnostics_relay_client_connect(provider.raw(), &out));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok(DiagnosticsRelay::adopt(out));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<DiagnosticsRelay, FfiError> DiagnosticsRelay::from_socket(Idevice&& socket) {
|
||||||
|
DiagnosticsRelayClientHandle* out = nullptr;
|
||||||
|
FfiError e(::diagnostics_relay_client_new(socket.raw(), &out));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
socket.release();
|
||||||
|
return Ok(DiagnosticsRelay::adopt(out));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------- API Methods --------
|
||||||
|
|
||||||
|
Result<Option<plist_t>, FfiError>
|
||||||
|
DiagnosticsRelay::ioregistry(Option<std::string> current_plane,
|
||||||
|
Option<std::string> entry_name,
|
||||||
|
Option<std::string> entry_class) const {
|
||||||
|
plist_t res = nullptr;
|
||||||
|
|
||||||
|
const char* plane_ptr = current_plane.is_some() ? current_plane.unwrap().c_str() : nullptr;
|
||||||
|
const char* name_ptr = entry_name.is_some() ? entry_name.unwrap().c_str() : nullptr;
|
||||||
|
const char* class_ptr = entry_class.is_some() ? entry_class.unwrap().c_str() : nullptr;
|
||||||
|
|
||||||
|
FfiError e(
|
||||||
|
::diagnostics_relay_client_ioregistry(handle_.get(), plane_ptr, name_ptr, class_ptr, &res));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res == nullptr) {
|
||||||
|
return Ok(Option<plist_t>(None));
|
||||||
|
}
|
||||||
|
return Ok(Some(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<Option<plist_t>, FfiError>
|
||||||
|
DiagnosticsRelay::mobilegestalt(Option<std::vector<char*>> keys) const {
|
||||||
|
plist_t res = nullptr;
|
||||||
|
|
||||||
|
if (!keys.is_some() || keys.unwrap().empty()) {
|
||||||
|
return Err(FfiError::InvalidArgument());
|
||||||
|
}
|
||||||
|
|
||||||
|
FfiError e(::diagnostics_relay_client_mobilegestalt(
|
||||||
|
handle_.get(), keys.unwrap().data(), keys.unwrap().size(), &res));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res == nullptr) {
|
||||||
|
return Ok(Option<plist_t>(None));
|
||||||
|
}
|
||||||
|
return Ok(Some(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<Option<plist_t>, FfiError> DiagnosticsRelay::gasguage() const {
|
||||||
|
plist_t res = nullptr;
|
||||||
|
FfiError e(::diagnostics_relay_client_gasguage(handle_.get(), &res));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res == nullptr) {
|
||||||
|
return Ok(Option<plist_t>(None));
|
||||||
|
}
|
||||||
|
return Ok(Some(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<Option<plist_t>, FfiError> DiagnosticsRelay::nand() const {
|
||||||
|
plist_t res = nullptr;
|
||||||
|
FfiError e(::diagnostics_relay_client_nand(handle_.get(), &res));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res == nullptr) {
|
||||||
|
return Ok(Option<plist_t>(None));
|
||||||
|
}
|
||||||
|
return Ok(Some(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<Option<plist_t>, FfiError> DiagnosticsRelay::all() const {
|
||||||
|
plist_t res = nullptr;
|
||||||
|
FfiError e(::diagnostics_relay_client_all(handle_.get(), &res));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res == nullptr) {
|
||||||
|
return Ok(Option<plist_t>(None));
|
||||||
|
}
|
||||||
|
return Ok(Some(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<Option<plist_t>, FfiError> DiagnosticsRelay::wifi() const {
|
||||||
|
plist_t res = nullptr;
|
||||||
|
FfiError e(::diagnostics_relay_client_wifi(handle_.get(), &res));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res == nullptr) {
|
||||||
|
return Ok(Option<plist_t>(None));
|
||||||
|
}
|
||||||
|
return Ok(Some(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void, FfiError> DiagnosticsRelay::restart() {
|
||||||
|
FfiError e(::diagnostics_relay_client_restart(handle_.get()));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void, FfiError> DiagnosticsRelay::shutdown() {
|
||||||
|
FfiError e(::diagnostics_relay_client_shutdown(handle_.get()));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void, FfiError> DiagnosticsRelay::sleep() {
|
||||||
|
FfiError e(::diagnostics_relay_client_sleep(handle_.get()));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void, FfiError> DiagnosticsRelay::goodbye() {
|
||||||
|
FfiError e(::diagnostics_relay_client_goodbye(handle_.get()));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace IdeviceFFI
|
||||||
@@ -50,6 +50,7 @@ tss = ["idevice/tss"]
|
|||||||
tunneld = ["idevice/tunneld"]
|
tunneld = ["idevice/tunneld"]
|
||||||
usbmuxd = ["idevice/usbmuxd"]
|
usbmuxd = ["idevice/usbmuxd"]
|
||||||
xpc = ["idevice/xpc"]
|
xpc = ["idevice/xpc"]
|
||||||
|
screenshotr = ["idevice/screenshotr"]
|
||||||
full = [
|
full = [
|
||||||
"afc",
|
"afc",
|
||||||
"amfi",
|
"amfi",
|
||||||
@@ -75,6 +76,7 @@ full = [
|
|||||||
"tunneld",
|
"tunneld",
|
||||||
"springboardservices",
|
"springboardservices",
|
||||||
"syslog_relay",
|
"syslog_relay",
|
||||||
|
"screenshotr",
|
||||||
]
|
]
|
||||||
default = ["full", "aws-lc"]
|
default = ["full", "aws-lc"]
|
||||||
|
|
||||||
|
|||||||
@@ -620,10 +620,10 @@ pub unsafe extern "C" fn afc_file_read(
|
|||||||
|
|
||||||
let fd = unsafe { &mut *(handle as *mut FileDescriptor) };
|
let fd = unsafe { &mut *(handle as *mut FileDescriptor) };
|
||||||
let res: Result<Vec<u8>, IdeviceError> = run_sync({
|
let res: Result<Vec<u8>, IdeviceError> = run_sync({
|
||||||
let mut buf = Vec::with_capacity(len);
|
let mut buf = vec![0u8; len];
|
||||||
async move {
|
async move {
|
||||||
let r = fd.read(&mut buf).await?;
|
let r = fd.read(&mut buf).await?;
|
||||||
buf.resize(r, 0);
|
buf.truncate(r);
|
||||||
Ok(buf)
|
Ok(buf)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ pub unsafe extern "C" fn diagnostics_relay_client_mobilegestalt(
|
|||||||
return ffi_err!(IdeviceError::FfiInvalidArg);
|
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||||
}
|
}
|
||||||
|
|
||||||
let keys = if keys.is_null() {
|
let keys = if !keys.is_null() {
|
||||||
let keys = unsafe { std::slice::from_raw_parts(keys, keys_len) };
|
let keys = unsafe { std::slice::from_raw_parts(keys, keys_len) };
|
||||||
Some(
|
Some(
|
||||||
keys.iter()
|
keys.iter()
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ mod pairing_file;
|
|||||||
pub mod provider;
|
pub mod provider;
|
||||||
#[cfg(feature = "xpc")]
|
#[cfg(feature = "xpc")]
|
||||||
pub mod rsd;
|
pub mod rsd;
|
||||||
|
#[cfg(feature = "screenshotr")]
|
||||||
|
pub mod screenshotr;
|
||||||
#[cfg(feature = "springboardservices")]
|
#[cfg(feature = "springboardservices")]
|
||||||
pub mod springboardservices;
|
pub mod springboardservices;
|
||||||
#[cfg(feature = "syslog_relay")]
|
#[cfg(feature = "syslog_relay")]
|
||||||
|
|||||||
@@ -250,6 +250,60 @@ pub unsafe extern "C" fn lockdownd_enter_recovery(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets a value in lockdownd
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `client` - A valid LockdowndClient handle
|
||||||
|
/// * `key` - The key to set (null-terminated string)
|
||||||
|
/// * `value` - The value to set as a plist
|
||||||
|
/// * `domain` - The domain to set in (null-terminated string, optional)
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// An IdeviceFfiError on error, null on success
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `client` must be a valid pointer to a handle allocated by this library
|
||||||
|
/// `key` must be a valid null-terminated string
|
||||||
|
/// `value` must be a valid plist
|
||||||
|
/// `domain` must be a valid null-terminated string or NULL
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn lockdownd_set_value(
|
||||||
|
client: *mut LockdowndClientHandle,
|
||||||
|
key: *const libc::c_char,
|
||||||
|
value: plist_t,
|
||||||
|
domain: *const libc::c_char,
|
||||||
|
) -> *mut IdeviceFfiError {
|
||||||
|
if client.is_null() || key.is_null() || value.is_null() {
|
||||||
|
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = match unsafe { std::ffi::CStr::from_ptr(key) }.to_str() {
|
||||||
|
Ok(k) => k,
|
||||||
|
Err(_) => return ffi_err!(IdeviceError::InvalidCString),
|
||||||
|
};
|
||||||
|
|
||||||
|
let domain = if domain.is_null() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(match unsafe { std::ffi::CStr::from_ptr(domain) }.to_str() {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => return ffi_err!(IdeviceError::InvalidCString),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let value = unsafe { &mut *value }.borrow_self().clone();
|
||||||
|
|
||||||
|
let res: Result<(), IdeviceError> = run_sync_local(async move {
|
||||||
|
let client_ref = unsafe { &mut (*client).0 };
|
||||||
|
client_ref.set_value(key, value, domain).await
|
||||||
|
});
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(_) => null_mut(),
|
||||||
|
Err(e) => ffi_err!(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Frees a LockdowndClient handle
|
/// Frees a LockdowndClient handle
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
|||||||
132
ffi/src/screenshotr.rs
Normal file
132
ffi/src/screenshotr.rs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
// Jackson Coxson
|
||||||
|
|
||||||
|
use std::ptr::null_mut;
|
||||||
|
|
||||||
|
use idevice::{
|
||||||
|
IdeviceError, IdeviceService, provider::IdeviceProvider,
|
||||||
|
services::screenshotr::ScreenshotService,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{IdeviceFfiError, ffi_err, provider::IdeviceProviderHandle, run_sync_local};
|
||||||
|
|
||||||
|
pub struct ScreenshotrClientHandle(pub ScreenshotService);
|
||||||
|
|
||||||
|
/// Represents a screenshot data buffer
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct ScreenshotData {
|
||||||
|
pub data: *mut u8,
|
||||||
|
pub length: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connects to screenshotr service using provider
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * [`provider`] - An IdeviceProvider
|
||||||
|
/// * [`client`] - On success, will be set to point to a newly allocated ScreenshotrClient handle
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// An IdeviceFfiError on error, null on success
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `provider` must be a valid pointer to a handle allocated by this library
|
||||||
|
/// `client` must be a valid, non-null pointer to a location where the handle will be stored
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn screenshotr_connect(
|
||||||
|
provider: *mut IdeviceProviderHandle,
|
||||||
|
client: *mut *mut ScreenshotrClientHandle,
|
||||||
|
) -> *mut IdeviceFfiError {
|
||||||
|
if provider.is_null() || client.is_null() {
|
||||||
|
tracing::error!("Null pointer provided");
|
||||||
|
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let res: Result<ScreenshotService, IdeviceError> = run_sync_local(async move {
|
||||||
|
let provider_ref: &dyn IdeviceProvider = unsafe { &*(*provider).0 };
|
||||||
|
ScreenshotService::connect(provider_ref).await
|
||||||
|
});
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(r) => {
|
||||||
|
let boxed = Box::new(ScreenshotrClientHandle(r));
|
||||||
|
unsafe { *client = Box::into_raw(boxed) };
|
||||||
|
null_mut()
|
||||||
|
}
|
||||||
|
Err(e) => ffi_err!(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Takes a screenshot from the device
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `client` - A valid ScreenshotrClient handle
|
||||||
|
/// * `screenshot` - Pointer to store the screenshot data
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// An IdeviceFfiError on error, null on success
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `client` must be a valid pointer to a handle allocated by this library
|
||||||
|
/// `screenshot` must be a valid pointer to store the screenshot data
|
||||||
|
/// The caller is responsible for freeing the screenshot data using screenshotr_screenshot_free
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn screenshotr_take_screenshot(
|
||||||
|
client: *mut ScreenshotrClientHandle,
|
||||||
|
screenshot: *mut ScreenshotData,
|
||||||
|
) -> *mut IdeviceFfiError {
|
||||||
|
if client.is_null() || screenshot.is_null() {
|
||||||
|
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let res: Result<Vec<u8>, IdeviceError> = run_sync_local(async move {
|
||||||
|
let client_ref = unsafe { &mut (*client).0 };
|
||||||
|
client_ref.take_screenshot().await
|
||||||
|
});
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(data) => {
|
||||||
|
let len = data.len();
|
||||||
|
let boxed = data.into_boxed_slice();
|
||||||
|
let ptr = Box::into_raw(boxed) as *mut u8;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
(*screenshot).data = ptr;
|
||||||
|
(*screenshot).length = len;
|
||||||
|
}
|
||||||
|
null_mut()
|
||||||
|
}
|
||||||
|
Err(e) => ffi_err!(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Frees screenshot data
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `screenshot` - The screenshot data to free
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `screenshot` must be a valid ScreenshotData that was allocated by screenshotr_take_screenshot
|
||||||
|
/// or NULL (in which case this function does nothing)
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn screenshotr_screenshot_free(screenshot: ScreenshotData) {
|
||||||
|
if !screenshot.data.is_null() && screenshot.length > 0 {
|
||||||
|
tracing::debug!("Freeing screenshot data");
|
||||||
|
let _ =
|
||||||
|
unsafe { Vec::from_raw_parts(screenshot.data, screenshot.length, screenshot.length) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Frees a ScreenshotrClient handle
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * [`handle`] - The handle to free
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `handle` must be a valid pointer to the handle that was allocated by this library,
|
||||||
|
/// or NULL (in which case this function does nothing)
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn screenshotr_client_free(handle: *mut ScreenshotrClientHandle) {
|
||||||
|
if !handle.is_null() {
|
||||||
|
tracing::debug!("Freeing screenshotr_client");
|
||||||
|
let _ = unsafe { Box::from_raw(handle) };
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user