mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +00:00
feat: Add support for 3pid management
This commit is contained in:
@@ -2069,10 +2069,12 @@
|
|||||||
#
|
#
|
||||||
#sender =
|
#sender =
|
||||||
|
|
||||||
# Whether to require that users provide an email address when they register.
|
# Whether to require that users provide an email address when they
|
||||||
|
# register.
|
||||||
#
|
#
|
||||||
#require_email_for_registration = false
|
#require_email_for_registration = false
|
||||||
|
|
||||||
# Whether to require that users who register with a registration token provide an email address.
|
# Whether to require that users who register with a registration token
|
||||||
|
# provide an email address.
|
||||||
#
|
#
|
||||||
#require_email_for_token_registration = false
|
#require_email_for_token_registration = false
|
||||||
|
|||||||
@@ -1121,7 +1121,7 @@ pub(super) async fn get_user_by_email(&self, email: String) -> Result {
|
|||||||
self.bail_restricted()?;
|
self.bail_restricted()?;
|
||||||
|
|
||||||
let Ok(email) = Address::try_from(email) else {
|
let Ok(email) = Address::try_from(email) else {
|
||||||
return Err!("Invalid email address");
|
return Err!("Invalid email address.");
|
||||||
};
|
};
|
||||||
|
|
||||||
match self.services.threepid.get_localpart_for_email(&email).await {
|
match self.services.threepid.get_localpart_for_email(&email).await {
|
||||||
@@ -1147,7 +1147,7 @@ pub(super) async fn change_email(&self, user_id: String, email: Option<String>)
|
|||||||
|
|
||||||
let user_id = parse_local_user_id(self.services, &user_id)?;
|
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||||
let Ok(new_email) = email.map(Address::try_from).transpose() else {
|
let Ok(new_email) = email.map(Address::try_from).transpose() else {
|
||||||
return Err!("Invalid email address");
|
return Err!("Invalid email address.");
|
||||||
};
|
};
|
||||||
|
|
||||||
if self.services.mailer.mailer().is_none() {
|
if self.services.mailer.mailer().is_none() {
|
||||||
|
|||||||
@@ -13,9 +13,8 @@ use ruma::{
|
|||||||
api::client::{
|
api::client::{
|
||||||
account::{
|
account::{
|
||||||
ThirdPartyIdRemovalStatus, change_password, check_registration_token_validity,
|
ThirdPartyIdRemovalStatus, change_password, check_registration_token_validity,
|
||||||
deactivate, get_3pids, get_username_availability,
|
deactivate, get_username_availability, request_password_change_token_via_email,
|
||||||
request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn,
|
whoami,
|
||||||
request_password_change_token_via_email, whoami,
|
|
||||||
},
|
},
|
||||||
uiaa::{AuthFlow, AuthType},
|
uiaa::{AuthFlow, AuthType},
|
||||||
},
|
},
|
||||||
@@ -33,6 +32,7 @@ use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH, join_room_by_id_helper};
|
|||||||
use crate::Ruma;
|
use crate::Ruma;
|
||||||
|
|
||||||
pub(crate) mod register;
|
pub(crate) mod register;
|
||||||
|
pub(crate) mod threepid;
|
||||||
|
|
||||||
/// # `GET /_matrix/client/v3/register/available`
|
/// # `GET /_matrix/client/v3/register/available`
|
||||||
///
|
///
|
||||||
@@ -226,12 +226,12 @@ pub(crate) async fn change_password_route(
|
|||||||
/// # `POST /_matrix/client/v3/account/password/email/requestToken`
|
/// # `POST /_matrix/client/v3/account/password/email/requestToken`
|
||||||
///
|
///
|
||||||
/// Requests a validation email for the purpose of resetting a user's password.
|
/// Requests a validation email for the purpose of resetting a user's password.
|
||||||
pub(crate) async fn password_request_token_route(
|
pub(crate) async fn request_password_change_token_via_email_route(
|
||||||
State(services): State<crate::State>,
|
State(services): State<crate::State>,
|
||||||
body: Ruma<request_password_change_token_via_email::v3::Request>,
|
body: Ruma<request_password_change_token_via_email::v3::Request>,
|
||||||
) -> Result<request_password_change_token_via_email::v3::Response> {
|
) -> Result<request_password_change_token_via_email::v3::Response> {
|
||||||
let Ok(email) = Address::try_from(body.email.clone()) else {
|
let Ok(email) = Address::try_from(body.email.clone()) else {
|
||||||
return Err!(Request(InvalidParam("Invalid email address")));
|
return Err!(Request(InvalidParam("Invalid email address.")));
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(localpart) = services.threepid.get_localpart_for_email(&email).await else {
|
let Some(localpart) = services.threepid.get_localpart_for_email(&email).await else {
|
||||||
@@ -341,45 +341,6 @@ pub(crate) async fn deactivate_route(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// # `GET _matrix/client/v3/account/3pid`
|
|
||||||
///
|
|
||||||
/// Get a list of third party identifiers associated with this account.
|
|
||||||
///
|
|
||||||
/// - Currently always returns empty list
|
|
||||||
pub(crate) async fn third_party_route(
|
|
||||||
body: Ruma<get_3pids::v3::Request>,
|
|
||||||
) -> Result<get_3pids::v3::Response> {
|
|
||||||
let _sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
|
||||||
|
|
||||||
Ok(get_3pids::v3::Response::new(Vec::new()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # `POST /_matrix/client/v3/account/3pid/email/requestToken`
|
|
||||||
///
|
|
||||||
/// "This API should be used to request validation tokens when adding an email
|
|
||||||
/// address to an account"
|
|
||||||
///
|
|
||||||
/// - 403 signals that The homeserver does not allow the third party identifier
|
|
||||||
/// as a contact option.
|
|
||||||
pub(crate) async fn request_3pid_management_token_via_email_route(
|
|
||||||
_body: Ruma<request_3pid_management_token_via_email::v3::Request>,
|
|
||||||
) -> Result<request_3pid_management_token_via_email::v3::Response> {
|
|
||||||
Err!(Request(ThreepidDenied("Third party identifiers are not implemented")))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # `POST /_matrix/client/v3/account/3pid/msisdn/requestToken`
|
|
||||||
///
|
|
||||||
/// "This API should be used to request validation tokens when adding an phone
|
|
||||||
/// number to an account"
|
|
||||||
///
|
|
||||||
/// - 403 signals that The homeserver does not allow the third party identifier
|
|
||||||
/// as a contact option.
|
|
||||||
pub(crate) async fn request_3pid_management_token_via_msisdn_route(
|
|
||||||
_body: Ruma<request_3pid_management_token_via_msisdn::v3::Request>,
|
|
||||||
) -> Result<request_3pid_management_token_via_msisdn::v3::Response> {
|
|
||||||
Err!(Request(ThreepidDenied("Third party identifiers are not implemented")))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # `GET /_matrix/client/v1/register/m.login.registration_token/validity`
|
/// # `GET /_matrix/client/v1/register/m.login.registration_token/validity`
|
||||||
///
|
///
|
||||||
/// Checks if the provided registration token is valid at the time of checking.
|
/// Checks if the provided registration token is valid at the time of checking.
|
||||||
|
|||||||
@@ -569,13 +569,13 @@ async fn determine_registration_user_id(
|
|||||||
|
|
||||||
/// # `POST /_matrix/client/v3/register/email/requestToken`
|
/// # `POST /_matrix/client/v3/register/email/requestToken`
|
||||||
///
|
///
|
||||||
/// Requests a validation email for the purpose of registering a new account
|
/// Requests a validation email for the purpose of registering a new account.
|
||||||
pub(crate) async fn register_request_token_route(
|
pub(crate) async fn request_registration_token_via_email_route(
|
||||||
State(services): State<crate::State>,
|
State(services): State<crate::State>,
|
||||||
body: Ruma<request_registration_token_via_email::v3::Request>,
|
body: Ruma<request_registration_token_via_email::v3::Request>,
|
||||||
) -> Result<request_registration_token_via_email::v3::Response> {
|
) -> Result<request_registration_token_via_email::v3::Response> {
|
||||||
let Ok(email) = Address::try_from(body.email.clone()) else {
|
let Ok(email) = Address::try_from(body.email.clone()) else {
|
||||||
return Err!(Request(InvalidParam("Invalid email address")));
|
return Err!(Request(InvalidParam("Invalid email address.")));
|
||||||
};
|
};
|
||||||
|
|
||||||
if services
|
if services
|
||||||
@@ -584,7 +584,7 @@ pub(crate) async fn register_request_token_route(
|
|||||||
.await
|
.await
|
||||||
.is_some()
|
.is_some()
|
||||||
{
|
{
|
||||||
return Err!(Request(ThreepidInUse("This email address is already in use")));
|
return Err!(Request(ThreepidInUse("This email address is already in use.")));
|
||||||
}
|
}
|
||||||
|
|
||||||
let session = services
|
let session = services
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
use axum::extract::State;
|
||||||
|
use conduwuit::{Err, Result, err};
|
||||||
|
use lettre::{Address, message::Mailbox};
|
||||||
|
use ruma::{
|
||||||
|
MilliSecondsSinceUnixEpoch,
|
||||||
|
api::client::account::{
|
||||||
|
ThirdPartyIdRemovalStatus, add_3pid, delete_3pid, get_3pids,
|
||||||
|
request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn,
|
||||||
|
},
|
||||||
|
thirdparty::{Medium, ThirdPartyIdentifierInit},
|
||||||
|
};
|
||||||
|
use service::{mailer::messages, uiaa::Identity};
|
||||||
|
|
||||||
|
use crate::Ruma;
|
||||||
|
|
||||||
|
/// # `GET _matrix/client/v3/account/3pid`
|
||||||
|
///
|
||||||
|
/// Get a list of third party identifiers associated with this account.
|
||||||
|
pub(crate) async fn third_party_route(
|
||||||
|
State(services): State<crate::State>,
|
||||||
|
body: Ruma<get_3pids::v3::Request>,
|
||||||
|
) -> Result<get_3pids::v3::Response> {
|
||||||
|
let sender_user = body.sender_user();
|
||||||
|
let mut threepids = vec![];
|
||||||
|
|
||||||
|
if let Some(email) = services
|
||||||
|
.threepid
|
||||||
|
.get_email_for_localpart(sender_user.localpart())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
threepids.push(
|
||||||
|
ThirdPartyIdentifierInit {
|
||||||
|
address: email.to_string(),
|
||||||
|
medium: Medium::Email,
|
||||||
|
// We don't currently track these, and they aren't used for much
|
||||||
|
validated_at: MilliSecondsSinceUnixEpoch::now(),
|
||||||
|
added_at: MilliSecondsSinceUnixEpoch::from_system_time(SystemTime::UNIX_EPOCH)
|
||||||
|
.unwrap(),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(get_3pids::v3::Response::new(threepids))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # `POST /_matrix/client/v3/account/3pid/email/requestToken`
|
||||||
|
///
|
||||||
|
/// Requests a validation email for the purpose of changing an account's email.
|
||||||
|
pub(crate) async fn request_3pid_management_token_via_email_route(
|
||||||
|
State(services): State<crate::State>,
|
||||||
|
body: Ruma<request_3pid_management_token_via_email::v3::Request>,
|
||||||
|
) -> Result<request_3pid_management_token_via_email::v3::Response> {
|
||||||
|
let Ok(email) = Address::try_from(body.email.clone()) else {
|
||||||
|
return Err!(Request(InvalidParam("Invalid email address.")));
|
||||||
|
};
|
||||||
|
|
||||||
|
if services
|
||||||
|
.threepid
|
||||||
|
.get_localpart_for_email(&email)
|
||||||
|
.await
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
return Err!(Request(ThreepidInUse("This email address is already in use.")));
|
||||||
|
}
|
||||||
|
|
||||||
|
let session = services
|
||||||
|
.threepid
|
||||||
|
.send_validation_email(
|
||||||
|
Mailbox::new(None, email),
|
||||||
|
|verification_link| messages::ChangeEmail {
|
||||||
|
server_name: services.config.server_name.as_str(),
|
||||||
|
user_id: body.sender_user.as_deref(),
|
||||||
|
verification_link,
|
||||||
|
},
|
||||||
|
&body.client_secret,
|
||||||
|
body.send_attempt.try_into().unwrap(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(request_3pid_management_token_via_email::v3::Response::new(session))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # `POST /_matrix/client/v3/account/3pid/msisdn/requestToken`
|
||||||
|
///
|
||||||
|
/// "This API should be used to request validation tokens when adding an email
|
||||||
|
/// address to an account"
|
||||||
|
///
|
||||||
|
/// - 403 signals that The homeserver does not allow the third party identifier
|
||||||
|
/// as a contact option.
|
||||||
|
pub(crate) async fn request_3pid_management_token_via_msisdn_route(
|
||||||
|
_body: Ruma<request_3pid_management_token_via_msisdn::v3::Request>,
|
||||||
|
) -> Result<request_3pid_management_token_via_msisdn::v3::Response> {
|
||||||
|
Err!(Request(ThreepidMediumNotSupported(
|
||||||
|
"MSISDN third-party identifiers are not supported."
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # `POST /_matrix/client/v3/account/3pid/add`
|
||||||
|
pub(crate) async fn add_3pid_route(
|
||||||
|
State(services): State<crate::State>,
|
||||||
|
body: Ruma<add_3pid::v3::Request>,
|
||||||
|
) -> Result<add_3pid::v3::Response> {
|
||||||
|
let sender_user = body.sender_user();
|
||||||
|
|
||||||
|
// Require password auth to add an email
|
||||||
|
services
|
||||||
|
.uiaa
|
||||||
|
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let email = services
|
||||||
|
.threepid
|
||||||
|
.consume_valid_session(&body.sid, &body.client_secret)
|
||||||
|
.await
|
||||||
|
.map_err(|message| err!(Request(ThreepidAuthFailed("{message}"))))?;
|
||||||
|
|
||||||
|
services
|
||||||
|
.threepid
|
||||||
|
.associate_localpart_email(sender_user.localpart(), &email)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(add_3pid::v3::Response::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # `POST /_matrix/client/v3/account/3pid/delete`
|
||||||
|
pub(crate) async fn delete_3pid_route(
|
||||||
|
State(services): State<crate::State>,
|
||||||
|
body: Ruma<delete_3pid::v3::Request>,
|
||||||
|
) -> Result<delete_3pid::v3::Response> {
|
||||||
|
let sender_user = body.sender_user();
|
||||||
|
|
||||||
|
if body.medium != Medium::Email {
|
||||||
|
return Ok(delete_3pid::v3::Response {
|
||||||
|
id_server_unbind_result: ThirdPartyIdRemovalStatus::NoSupport,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if services
|
||||||
|
.threepid
|
||||||
|
.disassociate_localpart_email(sender_user.localpart())
|
||||||
|
.await
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
return Err!(Request(ThreepidNotFound("Your account has no associated email.")));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(delete_3pid::v3::Response {
|
||||||
|
id_server_unbind_result: ThirdPartyIdRemovalStatus::Success,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -30,8 +30,10 @@ pub(crate) async fn get_capabilities_route(
|
|||||||
default: services.server.config.default_room_version.clone(),
|
default: services.server.config.default_room_version.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// we do not implement 3PID stuff
|
// Only allow 3pid changes if SMTP is configured
|
||||||
capabilities.thirdparty_id_changes = ThirdPartyIdChangesCapability { enabled: false };
|
capabilities.thirdparty_id_changes = ThirdPartyIdChangesCapability {
|
||||||
|
enabled: services.mailer.mailer().is_some(),
|
||||||
|
};
|
||||||
|
|
||||||
capabilities.get_login_token = GetLoginTokenCapability {
|
capabilities.get_login_token = GetLoginTokenCapability {
|
||||||
enabled: services.server.config.login_via_existing_session,
|
enabled: services.server.config.login_via_existing_session,
|
||||||
|
|||||||
+7
-5
@@ -29,7 +29,7 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
|
|||||||
.ruma_route(&client::get_supported_versions_route)
|
.ruma_route(&client::get_supported_versions_route)
|
||||||
.ruma_route(&client::get_register_available_route)
|
.ruma_route(&client::get_register_available_route)
|
||||||
.ruma_route(&client::register::register_route)
|
.ruma_route(&client::register::register_route)
|
||||||
.ruma_route(&client::register::register_request_token_route)
|
.ruma_route(&client::register::request_registration_token_via_email_route)
|
||||||
.ruma_route(&client::get_login_types_route)
|
.ruma_route(&client::get_login_types_route)
|
||||||
.ruma_route(&client::login_route)
|
.ruma_route(&client::login_route)
|
||||||
.ruma_route(&client::login_token_route)
|
.ruma_route(&client::login_token_route)
|
||||||
@@ -37,11 +37,13 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
|
|||||||
.ruma_route(&client::logout_route)
|
.ruma_route(&client::logout_route)
|
||||||
.ruma_route(&client::logout_all_route)
|
.ruma_route(&client::logout_all_route)
|
||||||
.ruma_route(&client::change_password_route)
|
.ruma_route(&client::change_password_route)
|
||||||
.ruma_route(&client::password_request_token_route)
|
.ruma_route(&client::request_password_change_token_via_email_route)
|
||||||
.ruma_route(&client::deactivate_route)
|
.ruma_route(&client::deactivate_route)
|
||||||
.ruma_route(&client::third_party_route)
|
.ruma_route(&client::threepid::third_party_route)
|
||||||
.ruma_route(&client::request_3pid_management_token_via_email_route)
|
.ruma_route(&client::threepid::request_3pid_management_token_via_email_route)
|
||||||
.ruma_route(&client::request_3pid_management_token_via_msisdn_route)
|
.ruma_route(&client::threepid::request_3pid_management_token_via_msisdn_route)
|
||||||
|
.ruma_route(&client::threepid::add_3pid_route)
|
||||||
|
.ruma_route(&client::threepid::delete_3pid_route)
|
||||||
.ruma_route(&client::check_registration_token_validity)
|
.ruma_route(&client::check_registration_token_validity)
|
||||||
.ruma_route(&client::get_capabilities_route)
|
.ruma_route(&client::get_capabilities_route)
|
||||||
.ruma_route(&client::get_pushrules_all_route)
|
.ruma_route(&client::get_pushrules_all_route)
|
||||||
|
|||||||
@@ -415,13 +415,6 @@ impl<'a, 'de: 'a> de::Deserializer<'de> for &'a mut Deserializer<'de> {
|
|||||||
tracing::instrument(level = "trace", skip_all, fields(?self.buf))
|
tracing::instrument(level = "trace", skip_all, fields(?self.buf))
|
||||||
)]
|
)]
|
||||||
fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||||
debug_assert_eq!(
|
|
||||||
conduwuit::debug::type_name::<V>(),
|
|
||||||
"serde_json::value::de::<impl serde_core::de::Deserialize for \
|
|
||||||
serde_json::value::Value>::deserialize::ValueVisitor",
|
|
||||||
"deserialize_any: type not expected"
|
|
||||||
);
|
|
||||||
|
|
||||||
match self.record_peek_byte() {
|
match self.record_peek_byte() {
|
||||||
| Some(b'{') => self.deserialize_map(visitor),
|
| Some(b'{') => self.deserialize_map(visitor),
|
||||||
| Some(b'[') => serde_json::Deserializer::from_slice(self.record_next())
|
| Some(b'[') => serde_json::Deserializer::from_slice(self.record_next())
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ pub trait MessageTemplate: Template {
|
|||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "mail/change_email.txt")]
|
#[template(path = "mail/change_email.txt")]
|
||||||
pub struct ChangeEmail<'a> {
|
pub struct ChangeEmail<'a> {
|
||||||
pub user_id: &'a UserId,
|
pub server_name: &'a str,
|
||||||
|
pub user_id: Option<&'a UserId>,
|
||||||
pub verification_link: String,
|
pub verification_link: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,12 @@
|
|||||||
|
|
||||||
{% block content -%}
|
{% block content -%}
|
||||||
Hello!
|
Hello!
|
||||||
|
{% if let Some(user_id) = user_id -%}
|
||||||
Somebody, probably you, tried to associate this email address with the Matrix account {{ user_id }}.
|
Somebody, probably you, tried to associate this email address with the Matrix account {{ user_id }}.
|
||||||
If that's your account, and this is your email address, click this link to proceed:
|
{%- else -%}
|
||||||
|
Somebody, probably you, tried to associate this email address with a Matrix account on {{ server_name }}.
|
||||||
|
{%- endif %}
|
||||||
|
If that was you, and this is your email address, click this link to proceed:
|
||||||
{{ verification_link }}
|
{{ verification_link }}
|
||||||
Otherwise, you can ignore this email. The above link will expire in one hour.
|
Otherwise, you can ignore this email. The above link will expire in one hour.
|
||||||
{%- endblock %}
|
{%- endblock %}
|
||||||
|
|||||||
@@ -227,15 +227,15 @@ impl Service {
|
|||||||
///
|
///
|
||||||
/// [`Self::get_localpart_for_email`] may be used if only the email is
|
/// [`Self::get_localpart_for_email`] may be used if only the email is
|
||||||
/// known.
|
/// known.
|
||||||
pub async fn disassociate_localpart_email(&self, localpart: &str) {
|
pub async fn disassociate_localpart_email(&self, localpart: &str) -> Option<Address> {
|
||||||
let email = self
|
let email = self.get_email_for_localpart(localpart).await?;
|
||||||
.get_email_for_localpart(localpart)
|
|
||||||
.await
|
|
||||||
.expect("localpart has no email associated");
|
|
||||||
self.db.localpart_email.remove(localpart);
|
self.db.localpart_email.remove(localpart);
|
||||||
self.db
|
self.db
|
||||||
.email_localpart
|
.email_localpart
|
||||||
.remove(<Address as AsRef<str>>::as_ref(&email));
|
.remove(<Address as AsRef<str>>::as_ref(&email));
|
||||||
|
|
||||||
|
Some(email)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the email associated with a localpart, if one exists.
|
/// Get the email associated with a localpart, if one exists.
|
||||||
|
|||||||
Reference in New Issue
Block a user