mirror of
https://github.com/nab138/isideload.git
synced 2026-03-02 06:26:16 +01:00
Fix clippy errors
This commit is contained in:
@@ -27,11 +27,11 @@ impl Application {
|
||||
let temp_path = temp_dir
|
||||
.join(path.file_name().unwrap().to_string_lossy().to_string() + "_extracted");
|
||||
if temp_path.exists() {
|
||||
std::fs::remove_dir_all(&temp_path).map_err(|e| Error::Filesystem(e))?;
|
||||
std::fs::remove_dir_all(&temp_path).map_err(Error::Filesystem)?;
|
||||
}
|
||||
std::fs::create_dir_all(&temp_path).map_err(|e| Error::Filesystem(e))?;
|
||||
std::fs::create_dir_all(&temp_path).map_err(Error::Filesystem)?;
|
||||
|
||||
let file = File::open(&path).map_err(|e| Error::Filesystem(e))?;
|
||||
let file = File::open(&path).map_err(Error::Filesystem)?;
|
||||
let mut archive = ZipArchive::new(file).map_err(|e| {
|
||||
Error::Generic(format!("Failed to open application archive: {}", e))
|
||||
})?;
|
||||
@@ -47,7 +47,7 @@ impl Application {
|
||||
})?
|
||||
.filter_map(Result::ok)
|
||||
.filter(|entry| entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false))
|
||||
.filter(|entry| entry.path().extension().map_or(false, |ext| ext == "app"))
|
||||
.filter(|entry| entry.path().extension().is_some_and(|ext| ext == "app"))
|
||||
.collect();
|
||||
if app_dirs.len() == 1 {
|
||||
bundle_path = app_dirs[0].path();
|
||||
|
||||
@@ -20,11 +20,10 @@ impl Bundle {
|
||||
pub fn new(bundle_dir: PathBuf) -> Result<Self, Error> {
|
||||
let mut bundle_path = bundle_dir;
|
||||
// Remove trailing slash/backslash
|
||||
if let Some(path_str) = bundle_path.to_str() {
|
||||
if path_str.ends_with('/') || path_str.ends_with('\\') {
|
||||
if let Some(path_str) = bundle_path.to_str()
|
||||
&& (path_str.ends_with('/') || path_str.ends_with('\\')) {
|
||||
bundle_path = PathBuf::from(&path_str[..path_str.len() - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
let info_plist_path = bundle_path.join("Info.plist");
|
||||
assert_bundle(
|
||||
@@ -158,16 +157,14 @@ fn find_dylibs(dir: &Path, bundle_root: &Path) -> Result<Vec<String>, Error> {
|
||||
.map_err(|e| Error::InvalidBundle(format!("Failed to get file type: {}", e)))?;
|
||||
|
||||
if file_type.is_file() {
|
||||
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
if name.ends_with(".dylib") {
|
||||
if let Some(name) = path.file_name().and_then(|n| n.to_str())
|
||||
&& name.ends_with(".dylib") {
|
||||
// Get relative path from bundle root
|
||||
if let Ok(relative_path) = path.strip_prefix(bundle_root) {
|
||||
if let Some(relative_str) = relative_path.to_str() {
|
||||
if let Ok(relative_path) = path.strip_prefix(bundle_root)
|
||||
&& let Some(relative_str) = relative_path.to_str() {
|
||||
libraries.push(relative_str.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if file_type.is_dir() {
|
||||
collect_dylibs(&path, bundle_root, libraries)?;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ impl CertificateIdentity {
|
||||
hasher.update(apple_id.as_bytes());
|
||||
let hash_string = hex::encode(hasher.finalize()).to_lowercase();
|
||||
let key_path = configuration_path.join("keys").join(hash_string);
|
||||
fs::create_dir_all(&key_path).map_err(|e| Error::Filesystem(e))?;
|
||||
fs::create_dir_all(&key_path).map_err(Error::Filesystem)?;
|
||||
|
||||
let key_file = key_path.join("key.pem");
|
||||
let cert_file = key_path.join("cert.pem");
|
||||
@@ -57,7 +57,7 @@ impl CertificateIdentity {
|
||||
let pem_data = key
|
||||
.private_key_to_pem_pkcs8()
|
||||
.map_err(|e| Error::Certificate(format!("Failed to encode private key: {}", e)))?;
|
||||
fs::write(&key_file, pem_data).map_err(|e| Error::Filesystem(e))?;
|
||||
fs::write(&key_file, pem_data).map_err(Error::Filesystem)?;
|
||||
key
|
||||
};
|
||||
|
||||
@@ -78,7 +78,7 @@ impl CertificateIdentity {
|
||||
let cert_pem = cert.to_pem().map_err(|e| {
|
||||
Error::Certificate(format!("Failed to encode certificate to PEM: {}", e))
|
||||
})?;
|
||||
fs::write(&cert_identity.cert_file, cert_pem).map_err(|e| Error::Filesystem(e))?;
|
||||
fs::write(&cert_identity.cert_file, cert_pem).map_err(Error::Filesystem)?;
|
||||
|
||||
return Ok(cert_identity);
|
||||
}
|
||||
@@ -108,16 +108,13 @@ impl CertificateIdentity {
|
||||
.iter()
|
||||
.filter(|c| c.machine_name == self.machine_name)
|
||||
{
|
||||
if let Ok(x509_cert) = X509::from_der(&cert.cert_content) {
|
||||
if let Ok(cert_public_key) = x509_cert.public_key() {
|
||||
if let Ok(cert_public_key_der) = cert_public_key.public_key_to_der() {
|
||||
if cert_public_key_der == our_public_key {
|
||||
if let Ok(x509_cert) = X509::from_der(&cert.cert_content)
|
||||
&& let Ok(cert_public_key) = x509_cert.public_key()
|
||||
&& let Ok(cert_public_key_der) = cert_public_key.public_key_to_der()
|
||||
&& cert_public_key_der == our_public_key {
|
||||
return Ok(x509_cert);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(Error::Certificate(
|
||||
"No matching certificate found".to_string(),
|
||||
))
|
||||
@@ -202,7 +199,7 @@ impl CertificateIdentity {
|
||||
let cert_pem = certificate.to_pem().map_err(|e| {
|
||||
Error::Certificate(format!("Failed to encode certificate to PEM: {}", e))
|
||||
})?;
|
||||
fs::write(&self.cert_file, cert_pem).map_err(|e| Error::Filesystem(e))?;
|
||||
fs::write(&self.cert_file, cert_pem).map_err(Error::Filesystem)?;
|
||||
|
||||
self.certificate = Some(certificate);
|
||||
|
||||
|
||||
@@ -264,10 +264,10 @@ impl DeveloperSession {
|
||||
.to_vec();
|
||||
|
||||
result.push(DevelopmentCertificate {
|
||||
name: name,
|
||||
name,
|
||||
certificate_id,
|
||||
serial_number: serial_number,
|
||||
machine_name: machine_name,
|
||||
serial_number,
|
||||
machine_name,
|
||||
cert_content,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,11 +12,11 @@ use crate::Error;
|
||||
pub async fn install_app(
|
||||
provider: &impl IdeviceProvider,
|
||||
app_path: &Path,
|
||||
progress_callback: impl Fn(u64) -> (),
|
||||
progress_callback: impl Fn(u64),
|
||||
) -> Result<(), Error> {
|
||||
let mut afc_client = AfcClient::connect(provider)
|
||||
.await
|
||||
.map_err(|e| Error::IdeviceError(e))?;
|
||||
.map_err(Error::IdeviceError)?;
|
||||
|
||||
let dir = format!(
|
||||
"PublicStaging/{}",
|
||||
@@ -26,7 +26,7 @@ pub async fn install_app(
|
||||
|
||||
let mut instproxy_client = InstallationProxyClient::connect(provider)
|
||||
.await
|
||||
.map_err(|e| Error::IdeviceError(e))?;
|
||||
.map_err(Error::IdeviceError)?;
|
||||
|
||||
let mut options = plist::Dictionary::new();
|
||||
options.insert("PackageType".to_string(), "Developer".into());
|
||||
@@ -40,7 +40,7 @@ pub async fn install_app(
|
||||
(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| Error::IdeviceError(e))?;
|
||||
.map_err(Error::IdeviceError)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -51,13 +51,13 @@ fn afc_upload_dir<'a>(
|
||||
afc_path: &'a str,
|
||||
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>> {
|
||||
Box::pin(async move {
|
||||
let entries = std::fs::read_dir(path).map_err(|e| Error::Filesystem(e))?;
|
||||
let entries = std::fs::read_dir(path).map_err(Error::Filesystem)?;
|
||||
afc_client
|
||||
.mk_dir(afc_path)
|
||||
.await
|
||||
.map_err(|e| Error::IdeviceError(e))?;
|
||||
.map_err(Error::IdeviceError)?;
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|e| Error::Filesystem(e))?;
|
||||
let entry = entry.map_err(Error::Filesystem)?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
let new_afc_path = format!(
|
||||
@@ -77,16 +77,16 @@ fn afc_upload_dir<'a>(
|
||||
idevice::afc::opcode::AfcFopenMode::WrOnly,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| Error::IdeviceError(e))?;
|
||||
let bytes = std::fs::read(&path).map_err(|e| Error::Filesystem(e))?;
|
||||
.map_err(Error::IdeviceError)?;
|
||||
let bytes = std::fs::read(&path).map_err(Error::Filesystem)?;
|
||||
file_handle
|
||||
.write_entire(&bytes)
|
||||
.await
|
||||
.map_err(|e| Error::IdeviceError(e))?;
|
||||
.map_err(Error::IdeviceError)?;
|
||||
file_handle
|
||||
.close()
|
||||
.await
|
||||
.map_err(|e| Error::IdeviceError(e))?;
|
||||
.map_err(Error::IdeviceError)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -54,28 +54,28 @@ impl SideloadLogger for DefaultLogger {
|
||||
}
|
||||
|
||||
/// Sideload configuration options.
|
||||
pub struct SideloadConfiguration {
|
||||
pub struct SideloadConfiguration<'a> {
|
||||
/// An arbitrary machine name to appear on the certificate (e.x. "YCode")
|
||||
pub machine_name: String,
|
||||
/// Logger for reporting progress and errors
|
||||
pub logger: Box<dyn SideloadLogger>,
|
||||
pub logger: &'a dyn SideloadLogger,
|
||||
/// Directory used to store intermediate artifacts (profiles, certs, etc.). This directory will not be cleared at the end.
|
||||
pub store_dir: std::path::PathBuf,
|
||||
/// Whether or not to revoke the certificate immediately after installation
|
||||
pub revoke_cert: bool,
|
||||
}
|
||||
|
||||
impl Default for SideloadConfiguration {
|
||||
impl Default for SideloadConfiguration<'_> {
|
||||
fn default() -> Self {
|
||||
SideloadConfiguration::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl SideloadConfiguration {
|
||||
impl<'a> SideloadConfiguration<'a> {
|
||||
pub fn new() -> Self {
|
||||
SideloadConfiguration {
|
||||
machine_name: "isideload".to_string(),
|
||||
logger: Box::new(DefaultLogger),
|
||||
logger: &DefaultLogger,
|
||||
store_dir: std::env::current_dir().unwrap(),
|
||||
revoke_cert: false,
|
||||
}
|
||||
@@ -86,7 +86,7 @@ impl SideloadConfiguration {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_logger(mut self, logger: Box<dyn SideloadLogger>) -> Self {
|
||||
pub fn set_logger(mut self, logger: &'a dyn SideloadLogger) -> Self {
|
||||
self.logger = logger;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::{
|
||||
};
|
||||
use std::{io::Write, path::PathBuf};
|
||||
|
||||
fn error_and_return(logger: &Box<dyn SideloadLogger>, error: Error) -> Result<(), Error> {
|
||||
fn error_and_return(logger: &dyn SideloadLogger, error: Error) -> Result<(), Error> {
|
||||
logger.error(&error);
|
||||
Err(error)
|
||||
}
|
||||
@@ -30,13 +30,13 @@ pub async fn sideload_app(
|
||||
device_provider: &impl IdeviceProvider,
|
||||
dev_session: &DeveloperSession,
|
||||
app_path: PathBuf,
|
||||
config: SideloadConfiguration,
|
||||
config: SideloadConfiguration<'_>,
|
||||
) -> Result<(), Error> {
|
||||
let logger = config.logger;
|
||||
let mut lockdown_client = match LockdownClient::connect(device_provider).await {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
return error_and_return(&logger, Error::IdeviceError(e));
|
||||
return error_and_return(logger, Error::IdeviceError(e));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -44,13 +44,13 @@ pub async fn sideload_app(
|
||||
lockdown_client
|
||||
.start_session(&pairing_file)
|
||||
.await
|
||||
.map_err(|e| Error::IdeviceError(e))?;
|
||||
.map_err(Error::IdeviceError)?;
|
||||
}
|
||||
|
||||
let device_name = lockdown_client
|
||||
.get_value(Some("DeviceName"), None)
|
||||
.await
|
||||
.map_err(|e| Error::IdeviceError(e))?
|
||||
.map_err(Error::IdeviceError)?
|
||||
.as_string()
|
||||
.ok_or(Error::Generic(
|
||||
"Failed to convert DeviceName to string".to_string(),
|
||||
@@ -60,7 +60,7 @@ pub async fn sideload_app(
|
||||
let device_uuid = lockdown_client
|
||||
.get_value(Some("UniqueDeviceID"), None)
|
||||
.await
|
||||
.map_err(|e| Error::IdeviceError(e))?
|
||||
.map_err(Error::IdeviceError)?
|
||||
.as_string()
|
||||
.ok_or(Error::Generic(
|
||||
"Failed to convert UniqueDeviceID to string".to_string(),
|
||||
@@ -70,17 +70,17 @@ pub async fn sideload_app(
|
||||
let team = match dev_session.get_team().await {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
return error_and_return(&logger, e);
|
||||
return error_and_return(logger, e);
|
||||
}
|
||||
};
|
||||
|
||||
logger.log("Successfully retrieved team");
|
||||
|
||||
ensure_device_registered(&logger, dev_session, &team, &device_uuid, &device_name).await?;
|
||||
ensure_device_registered(logger, dev_session, &team, &device_uuid, &device_name).await?;
|
||||
|
||||
let cert = match CertificateIdentity::new(
|
||||
&config.store_dir,
|
||||
&dev_session,
|
||||
dev_session,
|
||||
dev_session.account.apple_id.clone(),
|
||||
config.machine_name,
|
||||
)
|
||||
@@ -88,7 +88,7 @@ pub async fn sideload_app(
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
return error_and_return(&logger, e);
|
||||
return error_and_return(logger, e);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -100,7 +100,7 @@ pub async fn sideload_app(
|
||||
{
|
||||
Ok(ids) => ids,
|
||||
Err(e) => {
|
||||
return error_and_return(&logger, e);
|
||||
return error_and_return(logger, e);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -110,7 +110,7 @@ pub async fn sideload_app(
|
||||
Some(id) => id.to_string(),
|
||||
None => {
|
||||
return error_and_return(
|
||||
&logger,
|
||||
logger,
|
||||
Error::InvalidBundle("No bundle identifier found in IPA".to_string()),
|
||||
);
|
||||
}
|
||||
@@ -120,7 +120,7 @@ pub async fn sideload_app(
|
||||
Some(name) => name.to_string(),
|
||||
None => {
|
||||
return error_and_return(
|
||||
&logger,
|
||||
logger,
|
||||
Error::InvalidBundle("No bundle name found in IPA".to_string()),
|
||||
);
|
||||
}
|
||||
@@ -132,7 +132,7 @@ pub async fn sideload_app(
|
||||
if let Some(id) = ext.bundle_identifier() {
|
||||
if !(id.starts_with(&main_app_bundle_id) && id.len() > main_app_bundle_id.len()) {
|
||||
return error_and_return(
|
||||
&logger,
|
||||
logger,
|
||||
Error::InvalidBundle(format!(
|
||||
"Extension {} is not part of the main app bundle identifier: {}",
|
||||
ext.bundle_name().unwrap_or("Unknown"),
|
||||
@@ -150,7 +150,7 @@ pub async fn sideload_app(
|
||||
}
|
||||
app.bundle.set_bundle_identifier(&main_app_id_str);
|
||||
|
||||
let extension_refs: Vec<_> = app.bundle.app_extensions().into_iter().collect();
|
||||
let extension_refs: Vec<_> = app.bundle.app_extensions().iter().collect();
|
||||
let mut bundles_with_app_id = vec![&app.bundle];
|
||||
bundles_with_app_id.extend(extension_refs);
|
||||
|
||||
@@ -165,10 +165,11 @@ pub async fn sideload_app(
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Some(available) = list_app_id_response.available_quantity {
|
||||
if app_ids_to_register.len() > available.try_into().unwrap() {
|
||||
if let Some(available) = list_app_id_response.available_quantity
|
||||
&& app_ids_to_register.len() > available.try_into().unwrap()
|
||||
{
|
||||
return error_and_return(
|
||||
&logger,
|
||||
logger,
|
||||
Error::InvalidBundle(format!(
|
||||
"This app requires {} app ids, but you only have {} available",
|
||||
app_ids_to_register.len(),
|
||||
@@ -176,16 +177,15 @@ pub async fn sideload_app(
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for bundle in app_ids_to_register {
|
||||
let id = bundle.bundle_identifier().unwrap_or("");
|
||||
let name = bundle.bundle_name().unwrap_or("");
|
||||
if let Err(e) = dev_session
|
||||
.add_app_id(DeveloperDeviceType::Ios, &team, &name, &id)
|
||||
.add_app_id(DeveloperDeviceType::Ios, &team, name, id)
|
||||
.await
|
||||
{
|
||||
return error_and_return(&logger, e);
|
||||
return error_and_return(logger, e);
|
||||
}
|
||||
}
|
||||
list_app_id_response = match dev_session
|
||||
@@ -194,7 +194,7 @@ pub async fn sideload_app(
|
||||
{
|
||||
Ok(ids) => ids,
|
||||
Err(e) => {
|
||||
return error_and_return(&logger, e);
|
||||
return error_and_return(logger, e);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -215,7 +215,7 @@ pub async fn sideload_app(
|
||||
Some(id) => id,
|
||||
None => {
|
||||
return error_and_return(
|
||||
&logger,
|
||||
logger,
|
||||
Error::Generic(format!(
|
||||
"Main app ID {} not found in registered app IDs",
|
||||
main_app_id_str
|
||||
@@ -240,12 +240,12 @@ pub async fn sideload_app(
|
||||
let mut body = plist::Dictionary::new();
|
||||
body.insert("APG3427HIY".to_string(), plist::Value::Boolean(true));
|
||||
let new_features = match dev_session
|
||||
.update_app_id(DeveloperDeviceType::Ios, &team, &app_id, &body)
|
||||
.update_app_id(DeveloperDeviceType::Ios, &team, app_id, &body)
|
||||
.await
|
||||
{
|
||||
Ok(new_feats) => new_feats,
|
||||
Err(e) => {
|
||||
return error_and_return(&logger, e);
|
||||
return error_and_return(logger, e);
|
||||
}
|
||||
};
|
||||
app_id.features = new_features;
|
||||
@@ -267,7 +267,7 @@ pub async fn sideload_app(
|
||||
{
|
||||
Ok(groups) => groups,
|
||||
Err(e) => {
|
||||
return error_and_return(&logger, e);
|
||||
return error_and_return(logger, e);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -288,7 +288,7 @@ pub async fn sideload_app(
|
||||
{
|
||||
Ok(group) => group,
|
||||
Err(e) => {
|
||||
return error_and_return(&logger, e);
|
||||
return error_and_return(logger, e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -306,7 +306,7 @@ pub async fn sideload_app(
|
||||
)
|
||||
.await;
|
||||
if assign_res.is_err() {
|
||||
return error_and_return(&logger, assign_res.err().unwrap());
|
||||
return error_and_return(logger, assign_res.err().unwrap());
|
||||
}
|
||||
// let provisioning_profile = match account
|
||||
// // This doesn't seem right to me, but it's what Sideloader does... Shouldn't it be downloading the provisioning profile for this app ID, not the main?
|
||||
@@ -332,7 +332,7 @@ pub async fn sideload_app(
|
||||
{
|
||||
Ok(pp /* tee hee */) => pp,
|
||||
Err(e) => {
|
||||
return error_and_return(&logger, e);
|
||||
return error_and_return(logger, e);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -341,12 +341,12 @@ pub async fn sideload_app(
|
||||
.join(format!("{}.mobileprovision", main_app_id_str));
|
||||
|
||||
if profile_path.exists() {
|
||||
std::fs::remove_file(&profile_path).map_err(|e| Error::Filesystem(e))?;
|
||||
std::fs::remove_file(&profile_path).map_err(Error::Filesystem)?;
|
||||
}
|
||||
|
||||
let mut file = std::fs::File::create(&profile_path).map_err(|e| Error::Filesystem(e))?;
|
||||
let mut file = std::fs::File::create(&profile_path).map_err(Error::Filesystem)?;
|
||||
file.write_all(&provisioning_profile.encoded_profile)
|
||||
.map_err(|e| Error::Filesystem(e))?;
|
||||
.map_err(Error::Filesystem)?;
|
||||
|
||||
// Without this, zsign complains it can't find the provision file
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -368,7 +368,7 @@ pub async fn sideload_app(
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
return error_and_return(&logger, Error::ZSignError(e));
|
||||
return error_and_return(logger, Error::ZSignError(e));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -381,7 +381,7 @@ pub async fn sideload_app(
|
||||
})
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
return error_and_return(&logger, e);
|
||||
return error_and_return(logger, e);
|
||||
}
|
||||
|
||||
if config.revoke_cert {
|
||||
@@ -412,7 +412,7 @@ pub async fn sideload_app(
|
||||
logger.log("Certificate revoked");
|
||||
} else {
|
||||
return error_and_return(
|
||||
&logger,
|
||||
logger,
|
||||
Error::Certificate("No certificate to revoke".to_string()),
|
||||
);
|
||||
}
|
||||
@@ -422,7 +422,7 @@ pub async fn sideload_app(
|
||||
}
|
||||
|
||||
pub async fn ensure_device_registered(
|
||||
logger: &Box<dyn SideloadLogger>,
|
||||
logger: &dyn SideloadLogger,
|
||||
dev_session: &DeveloperSession,
|
||||
team: &DeveloperTeam,
|
||||
uuid: &str,
|
||||
|
||||
Reference in New Issue
Block a user