Make generic errors carry more details
This commit is contained in:
parent
0d77557ad6
commit
60a27b5b11
46 changed files with 368 additions and 208 deletions
37
Cargo.lock
generated
37
Cargo.lock
generated
|
@ -1005,6 +1005,8 @@ dependencies = [
|
||||||
"fedimovies-utils",
|
"fedimovies-utils",
|
||||||
"hex",
|
"hex",
|
||||||
"log",
|
"log",
|
||||||
|
"openssl",
|
||||||
|
"postgres-openssl",
|
||||||
"postgres-protocol",
|
"postgres-protocol",
|
||||||
"postgres-types",
|
"postgres-types",
|
||||||
"postgres_query",
|
"postgres_query",
|
||||||
|
@ -1860,6 +1862,15 @@ version = "0.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-src"
|
||||||
|
version = "111.25.3+1.1.1t"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "924757a6a226bf60da5f7dd0311a34d2b52283dd82ddeb103208ddc66362f80c"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-sys"
|
name = "openssl-sys"
|
||||||
version = "0.9.86"
|
version = "0.9.86"
|
||||||
|
@ -1868,6 +1879,7 @@ checksum = "992bac49bdbab4423199c654a5515bd2a6c6a23bf03f2dd3bdb7e5ae6259bc69"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
|
"openssl-src",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
@ -2046,6 +2058,19 @@ dependencies = [
|
||||||
"syn 2.0.15",
|
"syn 2.0.15",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "postgres-openssl"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1de0ea6504e07ca78355a6fb88ad0f36cafe9e696cbc6717f16a207f3a60be72"
|
||||||
|
dependencies = [
|
||||||
|
"futures",
|
||||||
|
"openssl",
|
||||||
|
"tokio",
|
||||||
|
"tokio-openssl",
|
||||||
|
"tokio-postgres",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "postgres-protocol"
|
name = "postgres-protocol"
|
||||||
version = "0.6.5"
|
version = "0.6.5"
|
||||||
|
@ -2928,6 +2953,18 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-openssl"
|
||||||
|
version = "0.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c08f9ffb7809f1b20c1b398d92acf4cc719874b3b2b2d9ea2f09b4a80350878a"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"openssl",
|
||||||
|
"openssl-sys",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-postgres"
|
name = "tokio-postgres"
|
||||||
version = "0.7.7"
|
version = "0.7.7"
|
||||||
|
|
|
@ -6,7 +6,7 @@ edition = "2021"
|
||||||
rust-version = "1.68"
|
rust-version = "1.68"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "mitractl"
|
name = "fedimoviesctl"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
|
@ -25,7 +25,11 @@ async fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
let db_config = config.database_url.parse().unwrap();
|
let db_config = config.database_url.parse().unwrap();
|
||||||
let db_client = &mut create_database_client(&db_config, config.tls_ca_file.as_ref().map(|p| p.as_path())).await;
|
let db_client = &mut create_database_client(
|
||||||
|
&db_config,
|
||||||
|
config.tls_ca_file.as_ref().map(|p| p.as_path()),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
apply_migrations(db_client).await;
|
apply_migrations(db_client).await;
|
||||||
|
|
||||||
match subcmd {
|
match subcmd {
|
||||||
|
|
|
@ -3,7 +3,8 @@ use super::migrate::apply_migrations;
|
||||||
use tokio_postgres::config::Config;
|
use tokio_postgres::config::Config;
|
||||||
use tokio_postgres::Client;
|
use tokio_postgres::Client;
|
||||||
|
|
||||||
const DEFAULT_CONNECTION_URL: &str = "postgres://fedimovies:fedimovies@127.0.0.1:55432/fedimovies-test";
|
const DEFAULT_CONNECTION_URL: &str =
|
||||||
|
"postgres://fedimovies:fedimovies@127.0.0.1:55432/fedimovies-test";
|
||||||
|
|
||||||
pub async fn create_test_database() -> Client {
|
pub async fn create_test_database() -> Client {
|
||||||
let connection_url =
|
let connection_url =
|
||||||
|
|
|
@ -255,13 +255,7 @@ pub async fn get_mention_notifications(
|
||||||
related_emojis = RELATED_EMOJIS,
|
related_emojis = RELATED_EMOJIS,
|
||||||
);
|
);
|
||||||
let rows = db_client
|
let rows = db_client
|
||||||
.query(
|
.query(&statement, &[&EventType::Mention, &i64::from(limit)])
|
||||||
&statement,
|
|
||||||
&[
|
|
||||||
&EventType::Mention,
|
|
||||||
&i64::from(limit),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
let notifications: Vec<Notification> = rows
|
let notifications: Vec<Notification> = rows
|
||||||
.iter()
|
.iter()
|
||||||
|
|
|
@ -49,6 +49,17 @@ pub fn create_rsa_sha256_signature(
|
||||||
Ok(signature)
|
Ok(signature)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// RSASSA-PKCS1-v1_5 signature
|
||||||
|
pub fn create_rsa_signature(
|
||||||
|
private_key: &RsaPrivateKey,
|
||||||
|
message: &str,
|
||||||
|
) -> Result<Vec<u8>, rsa::errors::Error> {
|
||||||
|
let digest = Sha256::digest(message.as_bytes());
|
||||||
|
let padding = PaddingScheme::new_pkcs1v15_sign(Some(Hash::SHA2_256));
|
||||||
|
let signature = private_key.sign(padding, &digest)?;
|
||||||
|
Ok(signature)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_message_digest(message: &str) -> String {
|
pub fn get_message_digest(message: &str) -> String {
|
||||||
let digest = Sha256::digest(message.as_bytes());
|
let digest = Sha256::digest(message.as_bytes());
|
||||||
let digest_b64 = base64::encode(digest);
|
let digest_b64 = base64::encode(digest);
|
||||||
|
|
|
@ -34,39 +34,39 @@ pub fn parse_identity_proof(
|
||||||
attachment: &ActorAttachment,
|
attachment: &ActorAttachment,
|
||||||
) -> Result<IdentityProof, ValidationError> {
|
) -> Result<IdentityProof, ValidationError> {
|
||||||
if attachment.object_type != IDENTITY_PROOF {
|
if attachment.object_type != IDENTITY_PROOF {
|
||||||
return Err(ValidationError("invalid attachment type"));
|
return Err(ValidationError("invalid attachment type".to_string()));
|
||||||
};
|
};
|
||||||
let proof_type_str = attachment
|
let proof_type_str = attachment
|
||||||
.signature_algorithm
|
.signature_algorithm
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or(ValidationError("missing proof type"))?;
|
.ok_or(ValidationError("missing proof type".to_string()))?;
|
||||||
let proof_type = match proof_type_str.as_str() {
|
let proof_type = match proof_type_str.as_str() {
|
||||||
PROOF_TYPE_ID_EIP191 => IdentityProofType::LegacyEip191IdentityProof,
|
PROOF_TYPE_ID_EIP191 => IdentityProofType::LegacyEip191IdentityProof,
|
||||||
PROOF_TYPE_ID_MINISIGN => IdentityProofType::LegacyMinisignIdentityProof,
|
PROOF_TYPE_ID_MINISIGN => IdentityProofType::LegacyMinisignIdentityProof,
|
||||||
_ => return Err(ValidationError("unsupported proof type")),
|
_ => return Err(ValidationError("unsupported proof type".to_string())),
|
||||||
};
|
};
|
||||||
let did = attachment
|
let did = attachment
|
||||||
.name
|
.name
|
||||||
.parse::<Did>()
|
.parse::<Did>()
|
||||||
.map_err(|_| ValidationError("invalid DID"))?;
|
.map_err(|_| ValidationError("invalid DID".to_string()))?;
|
||||||
let message =
|
let message = create_identity_claim(actor_id, &did)
|
||||||
create_identity_claim(actor_id, &did).map_err(|_| ValidationError("invalid claim"))?;
|
.map_err(|_| ValidationError("invalid claim".to_string()))?;
|
||||||
let signature = attachment
|
let signature = attachment
|
||||||
.signature_value
|
.signature_value
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or(ValidationError("missing signature"))?;
|
.ok_or(ValidationError("missing signature".to_string()))?;
|
||||||
match did {
|
match did {
|
||||||
Did::Key(ref did_key) => {
|
Did::Key(ref did_key) => {
|
||||||
if !matches!(proof_type, IdentityProofType::LegacyMinisignIdentityProof) {
|
if !matches!(proof_type, IdentityProofType::LegacyMinisignIdentityProof) {
|
||||||
return Err(ValidationError("incorrect proof type"));
|
return Err(ValidationError("incorrect proof type".to_string()));
|
||||||
};
|
};
|
||||||
let signature_bin = parse_minisign_signature(signature)
|
let signature_bin = parse_minisign_signature(signature)
|
||||||
.map_err(|_| ValidationError("invalid signature encoding"))?;
|
.map_err(|_| ValidationError("invalid signature encoding".to_string()))?;
|
||||||
verify_minisign_signature(did_key, &message, &signature_bin)
|
verify_minisign_signature(did_key, &message, &signature_bin)
|
||||||
.map_err(|_| ValidationError("invalid identity proof"))?;
|
.map_err(|_| ValidationError("invalid identity proof".to_string()))?;
|
||||||
}
|
}
|
||||||
Did::Pkh(ref _did_pkh) => {
|
Did::Pkh(ref _did_pkh) => {
|
||||||
return Err(ValidationError("incorrect proof type"));
|
return Err(ValidationError("incorrect proof type".to_string()));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let proof = IdentityProof {
|
let proof = IdentityProof {
|
||||||
|
@ -110,12 +110,12 @@ pub fn parse_payment_option(
|
||||||
attachment: &ActorAttachment,
|
attachment: &ActorAttachment,
|
||||||
) -> Result<PaymentOption, ValidationError> {
|
) -> Result<PaymentOption, ValidationError> {
|
||||||
if attachment.object_type != LINK {
|
if attachment.object_type != LINK {
|
||||||
return Err(ValidationError("invalid attachment type"));
|
return Err(ValidationError("invalid attachment type".to_string()));
|
||||||
};
|
};
|
||||||
let href = attachment
|
let href = attachment
|
||||||
.href
|
.href
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or(ValidationError("href attribute is required"))?
|
.ok_or(ValidationError("href attribute is required".to_string()))?
|
||||||
.to_string();
|
.to_string();
|
||||||
let payment_option = PaymentOption::Link(PaymentLink {
|
let payment_option = PaymentOption::Link(PaymentLink {
|
||||||
name: attachment.name.clone(),
|
name: attachment.name.clone(),
|
||||||
|
@ -137,12 +137,12 @@ pub fn attach_extra_field(field: ExtraField) -> ActorAttachment {
|
||||||
|
|
||||||
pub fn parse_extra_field(attachment: &ActorAttachment) -> Result<ExtraField, ValidationError> {
|
pub fn parse_extra_field(attachment: &ActorAttachment) -> Result<ExtraField, ValidationError> {
|
||||||
if attachment.object_type != PROPERTY_VALUE {
|
if attachment.object_type != PROPERTY_VALUE {
|
||||||
return Err(ValidationError("invalid attachment type"));
|
return Err(ValidationError("invalid attachment type".to_string()));
|
||||||
};
|
};
|
||||||
let property_value = attachment
|
let property_value = attachment
|
||||||
.value
|
.value
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or(ValidationError("missing property value"))?;
|
.ok_or(ValidationError("missing property value".to_string()))?;
|
||||||
let field = ExtraField {
|
let field = ExtraField {
|
||||||
name: attachment.name.clone(),
|
name: attachment.name.clone(),
|
||||||
value: property_value.to_string(),
|
value: property_value.to_string(),
|
||||||
|
|
|
@ -13,6 +13,7 @@ use fedimovies_utils::{
|
||||||
urls::get_hostname,
|
urls::get_hostname,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::activitypub::types::build_default_context;
|
||||||
use crate::activitypub::{
|
use crate::activitypub::{
|
||||||
constants::{
|
constants::{
|
||||||
AP_CONTEXT, MASTODON_CONTEXT, MITRA_CONTEXT, SCHEMA_ORG_CONTEXT, W3ID_SECURITY_CONTEXT,
|
AP_CONTEXT, MASTODON_CONTEXT, MITRA_CONTEXT, SCHEMA_ORG_CONTEXT, W3ID_SECURITY_CONTEXT,
|
||||||
|
@ -23,7 +24,6 @@ use crate::activitypub::{
|
||||||
types::deserialize_value_array,
|
types::deserialize_value_array,
|
||||||
vocabulary::{IDENTITY_PROOF, IMAGE, LINK, PERSON, PROPERTY_VALUE, SERVICE},
|
vocabulary::{IDENTITY_PROOF, IMAGE, LINK, PERSON, PROPERTY_VALUE, SERVICE},
|
||||||
};
|
};
|
||||||
use crate::activitypub::types::build_default_context;
|
|
||||||
use crate::errors::ValidationError;
|
use crate::errors::ValidationError;
|
||||||
use crate::media::get_file_url;
|
use crate::media::get_file_url;
|
||||||
use crate::webfinger::types::ActorAddress;
|
use crate::webfinger::types::ActorAddress;
|
||||||
|
@ -110,6 +110,7 @@ pub struct Actor {
|
||||||
pub inbox: String,
|
pub inbox: String,
|
||||||
pub outbox: String,
|
pub outbox: String,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
pub bot: bool,
|
pub bot: bool,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
@ -162,7 +163,8 @@ pub struct Actor {
|
||||||
|
|
||||||
impl Actor {
|
impl Actor {
|
||||||
pub fn address(&self) -> Result<ActorAddress, ValidationError> {
|
pub fn address(&self) -> Result<ActorAddress, ValidationError> {
|
||||||
let hostname = get_hostname(&self.id).map_err(|_| ValidationError("invalid actor ID"))?;
|
let hostname =
|
||||||
|
get_hostname(&self.id).map_err(|_| ValidationError("invalid actor ID".to_string()))?;
|
||||||
let actor_address = ActorAddress {
|
let actor_address = ActorAddress {
|
||||||
username: self.preferred_username.clone(),
|
username: self.preferred_username.clone(),
|
||||||
hostname: hostname,
|
hostname: hostname,
|
||||||
|
@ -203,7 +205,10 @@ impl Actor {
|
||||||
let attachment = match serde_json::from_value(attachment_value.clone()) {
|
let attachment = match serde_json::from_value(attachment_value.clone()) {
|
||||||
Ok(attachment) => attachment,
|
Ok(attachment) => attachment,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
log_error(attachment_type, ValidationError("invalid attachment"));
|
log_error(
|
||||||
|
attachment_type,
|
||||||
|
ValidationError("invalid attachment".to_string()),
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -229,7 +234,7 @@ impl Actor {
|
||||||
_ => {
|
_ => {
|
||||||
log_error(
|
log_error(
|
||||||
attachment_type,
|
attachment_type,
|
||||||
ValidationError("unsupported attachment type"),
|
ValidationError("unsupported attachment type".to_string()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,7 +7,7 @@ use fedimovies_models::{
|
||||||
profiles::queries::get_profile_by_remote_actor_id,
|
profiles::queries::get_profile_by_remote_actor_id,
|
||||||
profiles::types::DbActorProfile,
|
profiles::types::DbActorProfile,
|
||||||
};
|
};
|
||||||
use fedimovies_utils::{crypto_rsa::deserialize_public_key};
|
use fedimovies_utils::crypto_rsa::deserialize_public_key;
|
||||||
|
|
||||||
use crate::http_signatures::verify::{
|
use crate::http_signatures::verify::{
|
||||||
parse_http_signature, verify_http_signature,
|
parse_http_signature, verify_http_signature,
|
||||||
|
@ -16,9 +16,8 @@ use crate::http_signatures::verify::{
|
||||||
use crate::json_signatures::{
|
use crate::json_signatures::{
|
||||||
proofs::ProofType,
|
proofs::ProofType,
|
||||||
verify::{
|
verify::{
|
||||||
get_json_signature,
|
get_json_signature, verify_rsa_json_signature,
|
||||||
verify_rsa_json_signature, JsonSignatureVerificationError as JsonSignatureError,
|
JsonSignatureVerificationError as JsonSignatureError, JsonSigner,
|
||||||
JsonSigner,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use crate::media::MediaStorage;
|
use crate::media::MediaStorage;
|
||||||
|
|
|
@ -214,7 +214,7 @@ pub async fn import_post(
|
||||||
};
|
};
|
||||||
let object = fetch_object(instance, &object_id).await.map_err(|err| {
|
let object = fetch_object(instance, &object_id).await.map_err(|err| {
|
||||||
log::warn!("{}", err);
|
log::warn!("{}", err);
|
||||||
ValidationError("failed to fetch object")
|
ValidationError("failed to fetch object".into())
|
||||||
})?;
|
})?;
|
||||||
log::info!("fetched object {}", object.id);
|
log::info!("fetched object {}", object.id);
|
||||||
fetch_count += 1;
|
fetch_count += 1;
|
||||||
|
@ -278,9 +278,9 @@ pub async fn import_from_outbox(
|
||||||
let activities = fetch_outbox(&instance, &actor.outbox, limit).await?;
|
let activities = fetch_outbox(&instance, &actor.outbox, limit).await?;
|
||||||
log::info!("fetched {} activities", activities.len());
|
log::info!("fetched {} activities", activities.len());
|
||||||
for activity in activities {
|
for activity in activities {
|
||||||
let activity_actor = activity["actor"]
|
let activity_actor = activity["actor"].as_str().ok_or(ValidationError(
|
||||||
.as_str()
|
"actor property is missing from activity".to_string(),
|
||||||
.ok_or(ValidationError("actor property is missing"))?;
|
))?;
|
||||||
if activity_actor != actor.id {
|
if activity_actor != actor.id {
|
||||||
log::warn!("activity doesn't belong to outbox owner");
|
log::warn!("activity doesn't belong to outbox owner");
|
||||||
continue;
|
continue;
|
||||||
|
|
|
@ -29,13 +29,17 @@ pub async fn handle_accept(
|
||||||
activity: Value,
|
activity: Value,
|
||||||
) -> HandlerResult {
|
) -> HandlerResult {
|
||||||
// Accept(Follow)
|
// Accept(Follow)
|
||||||
let activity: Accept = serde_json::from_value(activity)
|
let activity: Accept = serde_json::from_value(activity.clone()).map_err(|_| {
|
||||||
.map_err(|_| ValidationError("unexpected activity structure"))?;
|
ValidationError(format!(
|
||||||
|
"unexpected Accept activity structure: {}",
|
||||||
|
activity
|
||||||
|
))
|
||||||
|
})?;
|
||||||
let actor_profile = get_profile_by_remote_actor_id(db_client, &activity.actor).await?;
|
let actor_profile = get_profile_by_remote_actor_id(db_client, &activity.actor).await?;
|
||||||
let follow_request_id = parse_local_object_id(&config.instance_url(), &activity.object)?;
|
let follow_request_id = parse_local_object_id(&config.instance_url(), &activity.object)?;
|
||||||
let follow_request = get_follow_request_by_id(db_client, &follow_request_id).await?;
|
let follow_request = get_follow_request_by_id(db_client, &follow_request_id).await?;
|
||||||
if follow_request.target_id != actor_profile.id {
|
if follow_request.target_id != actor_profile.id {
|
||||||
return Err(ValidationError("actor is not a target").into());
|
return Err(ValidationError("actor is not a target".to_string()).into());
|
||||||
};
|
};
|
||||||
if matches!(follow_request.request_status, FollowRequestStatus::Accepted) {
|
if matches!(follow_request.request_status, FollowRequestStatus::Accepted) {
|
||||||
// Ignore Accept if follow request already accepted
|
// Ignore Accept if follow request already accepted
|
||||||
|
|
|
@ -23,8 +23,8 @@ pub async fn handle_add(
|
||||||
db_client: &mut impl DatabaseClient,
|
db_client: &mut impl DatabaseClient,
|
||||||
activity: Value,
|
activity: Value,
|
||||||
) -> HandlerResult {
|
) -> HandlerResult {
|
||||||
let activity: Add = serde_json::from_value(activity)
|
let activity: Add = serde_json::from_value(activity.clone())
|
||||||
.map_err(|_| ValidationError("unexpected activity structure"))?;
|
.map_err(|_| ValidationError(format!("unexpected Add activity structure: {}", activity)))?;
|
||||||
let actor_profile = get_profile_by_remote_actor_id(db_client, &activity.actor).await?;
|
let actor_profile = get_profile_by_remote_actor_id(db_client, &activity.actor).await?;
|
||||||
let actor = actor_profile.actor_json.ok_or(HandlerError::LocalObject)?;
|
let actor = actor_profile.actor_json.ok_or(HandlerError::LocalObject)?;
|
||||||
if Some(activity.target) == actor.subscribers {
|
if Some(activity.target) == actor.subscribers {
|
||||||
|
|
|
@ -38,8 +38,8 @@ pub async fn handle_announce(
|
||||||
// https://codeberg.org/fediverse/fep/src/branch/main/feps/fep-1b12.md
|
// https://codeberg.org/fediverse/fep/src/branch/main/feps/fep-1b12.md
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
let activity: Announce = serde_json::from_value(activity)
|
let activity: Announce = serde_json::from_value(activity.clone())
|
||||||
.map_err(|_| ValidationError("unexpected activity structure"))?;
|
.map_err(|_| ValidationError(format!("unexpected activity structure: {}", activity)))?;
|
||||||
let repost_object_id = activity.id;
|
let repost_object_id = activity.id;
|
||||||
match get_post_by_remote_object_id(db_client, &repost_object_id).await {
|
match get_post_by_remote_object_id(db_client, &repost_object_id).await {
|
||||||
Ok(_) => return Ok(None), // Ignore if repost already exists
|
Ok(_) => return Ok(None), // Ignore if repost already exists
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
use log::warn;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::Value as JsonValue;
|
use serde_json::Value as JsonValue;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
@ -51,11 +52,11 @@ fn get_object_attributed_to(object: &Object) -> Result<String, ValidationError>
|
||||||
let attributed_to = object
|
let attributed_to = object
|
||||||
.attributed_to
|
.attributed_to
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or(ValidationError("unattributed note"))?;
|
.ok_or(ValidationError("unattributed note".to_string()))?;
|
||||||
let author_id = parse_array(attributed_to)
|
let author_id = parse_array(attributed_to)
|
||||||
.map_err(|_| ValidationError("invalid attributedTo property"))?
|
.map_err(|_| ValidationError("invalid attributedTo property".to_string()))?
|
||||||
.get(0)
|
.get(0)
|
||||||
.ok_or(ValidationError("invalid attributedTo property"))?
|
.ok_or(ValidationError("invalid attributedTo property".to_string()))?
|
||||||
.to_string();
|
.to_string();
|
||||||
Ok(author_id)
|
Ok(author_id)
|
||||||
}
|
}
|
||||||
|
@ -65,7 +66,7 @@ pub fn get_object_url(object: &Object) -> Result<String, ValidationError> {
|
||||||
Some(JsonValue::String(string)) => Some(string.to_owned()),
|
Some(JsonValue::String(string)) => Some(string.to_owned()),
|
||||||
Some(other_value) => {
|
Some(other_value) => {
|
||||||
let links: Vec<Link> = parse_property_value(other_value)
|
let links: Vec<Link> = parse_property_value(other_value)
|
||||||
.map_err(|_| ValidationError("invalid object URL"))?;
|
.map_err(|_| ValidationError("invalid object URL".to_string()))?;
|
||||||
links.get(0).map(|link| link.href.clone())
|
links.get(0).map(|link| link.href.clone())
|
||||||
}
|
}
|
||||||
None => None,
|
None => None,
|
||||||
|
@ -87,7 +88,7 @@ pub fn get_object_content(object: &Object) -> Result<String, ValidationError> {
|
||||||
object.name.as_deref().unwrap_or("").to_string()
|
object.name.as_deref().unwrap_or("").to_string()
|
||||||
};
|
};
|
||||||
if content.len() > CONTENT_MAX_SIZE {
|
if content.len() > CONTENT_MAX_SIZE {
|
||||||
return Err(ValidationError("content is too long"));
|
return Err(ValidationError("content is too long".to_string()));
|
||||||
};
|
};
|
||||||
let content_safe = clean_html(&content, content_allowed_classes());
|
let content_safe = clean_html(&content, content_allowed_classes());
|
||||||
Ok(content_safe)
|
Ok(content_safe)
|
||||||
|
@ -122,7 +123,7 @@ pub async fn get_object_attachments(
|
||||||
let mut unprocessed = vec![];
|
let mut unprocessed = vec![];
|
||||||
if let Some(ref value) = object.attachment {
|
if let Some(ref value) = object.attachment {
|
||||||
let list: Vec<Attachment> = parse_property_value(value)
|
let list: Vec<Attachment> = parse_property_value(value)
|
||||||
.map_err(|_| ValidationError("invalid attachment property"))?;
|
.map_err(|_| ValidationError(format!("invalid attachment property: {value:?}")))?;
|
||||||
let mut downloaded = vec![];
|
let mut downloaded = vec![];
|
||||||
for attachment in list {
|
for attachment in list {
|
||||||
match attachment.attachment_type.as_str() {
|
match attachment.attachment_type.as_str() {
|
||||||
|
@ -138,7 +139,7 @@ pub async fn get_object_attachments(
|
||||||
};
|
};
|
||||||
let attachment_url = attachment
|
let attachment_url = attachment
|
||||||
.url
|
.url
|
||||||
.ok_or(ValidationError("attachment URL is missing"))?;
|
.ok_or(ValidationError("attachment URL is missing".to_string()))?;
|
||||||
let (file_name, file_size, maybe_media_type) = match fetch_file(
|
let (file_name, file_size, maybe_media_type) = match fetch_file(
|
||||||
instance,
|
instance,
|
||||||
&attachment_url,
|
&attachment_url,
|
||||||
|
@ -155,8 +156,12 @@ pub async fn get_object_attachments(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Err(other_error) => {
|
Err(other_error) => {
|
||||||
log::warn!("{}", other_error);
|
log::warn!("failed to fetch attachment: {}", other_error);
|
||||||
return Err(ValidationError("failed to fetch attachment").into());
|
return Err(ValidationError(format!(
|
||||||
|
"failed to fetch attachment: {}",
|
||||||
|
other_error
|
||||||
|
))
|
||||||
|
.into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
log::info!("downloaded attachment {}", attachment_url);
|
log::info!("downloaded attachment {}", attachment_url);
|
||||||
|
@ -281,7 +286,8 @@ pub async fn handle_emoji(
|
||||||
let emoji = if let Some(emoji_id) = maybe_emoji_id {
|
let emoji = if let Some(emoji_id) = maybe_emoji_id {
|
||||||
update_emoji(db_client, &emoji_id, image, &tag.updated).await?
|
update_emoji(db_client, &emoji_id, image, &tag.updated).await?
|
||||||
} else {
|
} else {
|
||||||
let hostname = get_hostname(&tag.id).map_err(|_| ValidationError("invalid emoji ID"))?;
|
let hostname = get_hostname(&tag.id)
|
||||||
|
.map_err(|_| ValidationError(format!("invalid emoji tag ID: {}", tag.id)))?;
|
||||||
match create_emoji(
|
match create_emoji(
|
||||||
db_client,
|
db_client,
|
||||||
emoji_name,
|
emoji_name,
|
||||||
|
@ -351,9 +357,7 @@ pub async fn get_object_tags(
|
||||||
if let Ok(username) = parse_local_actor_id(&instance.url(), &href) {
|
if let Ok(username) = parse_local_actor_id(&instance.url(), &href) {
|
||||||
// Check if local Movie account exists and if not, create the movie, if valid.
|
// Check if local Movie account exists and if not, create the movie, if valid.
|
||||||
let user = match get_user_by_name(db_client, &username).await {
|
let user = match get_user_by_name(db_client, &username).await {
|
||||||
Ok(user) => {
|
Ok(user) => user,
|
||||||
user
|
|
||||||
},
|
|
||||||
Err(DatabaseError::NotFound(_)) => {
|
Err(DatabaseError::NotFound(_)) => {
|
||||||
if let Some(api_key) = &api_key {
|
if let Some(api_key) = &api_key {
|
||||||
log::warn!("failed to find mentioned user by name {}, checking if its a valid movie...", username);
|
log::warn!("failed to find mentioned user by name {}, checking if its a valid movie...", username);
|
||||||
|
@ -484,15 +488,13 @@ pub async fn get_object_tags(
|
||||||
|
|
||||||
fn get_audience(object: &Object) -> Result<Vec<String>, ValidationError> {
|
fn get_audience(object: &Object) -> Result<Vec<String>, ValidationError> {
|
||||||
let primary_audience = match object.to {
|
let primary_audience = match object.to {
|
||||||
Some(ref value) => {
|
Some(ref value) => parse_array(value)
|
||||||
parse_array(value).map_err(|_| ValidationError("invalid 'to' property value"))?
|
.map_err(|_| ValidationError("invalid 'to' property value".to_string()))?,
|
||||||
}
|
|
||||||
None => vec![],
|
None => vec![],
|
||||||
};
|
};
|
||||||
let secondary_audience = match object.cc {
|
let secondary_audience = match object.cc {
|
||||||
Some(ref value) => {
|
Some(ref value) => parse_array(value)
|
||||||
parse_array(value).map_err(|_| ValidationError("invalid 'cc' property value"))?
|
.map_err(|_| ValidationError("invalid 'cc' property value".to_string()))?,
|
||||||
}
|
|
||||||
None => vec![],
|
None => vec![],
|
||||||
};
|
};
|
||||||
let audience = [primary_audience, secondary_audience].concat();
|
let audience = [primary_audience, secondary_audience].concat();
|
||||||
|
@ -543,12 +545,17 @@ pub async fn handle_note(
|
||||||
log::info!("processing object of type {}", object.object_type);
|
log::info!("processing object of type {}", object.object_type);
|
||||||
}
|
}
|
||||||
other_type => {
|
other_type => {
|
||||||
log::warn!("discarding object of type {}", other_type);
|
let msg = format!("discarding object of type {}", other_type);
|
||||||
return Err(ValidationError("unsupported object type").into());
|
log::warn!("{msg}");
|
||||||
|
return Err(ValidationError(msg).into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if object.id.len() > OBJECT_ID_SIZE_MAX {
|
if object.id.len() > OBJECT_ID_SIZE_MAX {
|
||||||
return Err(ValidationError("object ID is too long").into());
|
return Err(ValidationError(format!(
|
||||||
|
"object ID is too long, {} of length",
|
||||||
|
object.id.len()
|
||||||
|
))
|
||||||
|
.into());
|
||||||
};
|
};
|
||||||
|
|
||||||
let author_id = get_object_attributed_to(&object)?;
|
let author_id = get_object_attributed_to(&object)?;
|
||||||
|
@ -571,7 +578,12 @@ pub async fn handle_note(
|
||||||
content += &create_content_link(attachment_url);
|
content += &create_content_link(attachment_url);
|
||||||
}
|
}
|
||||||
if content.is_empty() && attachments.is_empty() {
|
if content.is_empty() && attachments.is_empty() {
|
||||||
return Err(ValidationError("post is empty").into());
|
return Err(ValidationError(format!(
|
||||||
|
"post is empty (content={}, attachments={})",
|
||||||
|
content.is_empty(),
|
||||||
|
attachments.is_empty()
|
||||||
|
))
|
||||||
|
.into());
|
||||||
};
|
};
|
||||||
|
|
||||||
let (mentions, hashtags, links, emojis) = get_object_tags(
|
let (mentions, hashtags, links, emojis) = get_object_tags(
|
||||||
|
@ -652,8 +664,8 @@ pub async fn handle_create(
|
||||||
activity: JsonValue,
|
activity: JsonValue,
|
||||||
mut is_authenticated: bool,
|
mut is_authenticated: bool,
|
||||||
) -> HandlerResult {
|
) -> HandlerResult {
|
||||||
let activity: CreateNote =
|
let activity: CreateNote = serde_json::from_value(activity.clone())
|
||||||
serde_json::from_value(activity).map_err(|_| ValidationError("invalid object"))?;
|
.map_err(|_| ValidationError(format!("invalid CreateNote activity: {}", activity)))?;
|
||||||
let object = activity.object;
|
let object = activity.object;
|
||||||
|
|
||||||
// Verify attribution
|
// Verify attribution
|
||||||
|
|
|
@ -29,8 +29,12 @@ pub async fn handle_delete(
|
||||||
db_client: &mut impl DatabaseClient,
|
db_client: &mut impl DatabaseClient,
|
||||||
activity: Value,
|
activity: Value,
|
||||||
) -> HandlerResult {
|
) -> HandlerResult {
|
||||||
let activity: Delete = serde_json::from_value(activity)
|
let activity: Delete = serde_json::from_value(activity.clone()).map_err(|_| {
|
||||||
.map_err(|_| ValidationError("unexpected activity structure"))?;
|
ValidationError(format!(
|
||||||
|
"unexpected Delete activity structure: {}",
|
||||||
|
activity
|
||||||
|
))
|
||||||
|
})?;
|
||||||
if activity.object == activity.actor {
|
if activity.object == activity.actor {
|
||||||
// Self-delete
|
// Self-delete
|
||||||
let profile = match get_profile_by_remote_actor_id(db_client, &activity.object).await {
|
let profile = match get_profile_by_remote_actor_id(db_client, &activity.object).await {
|
||||||
|
@ -55,7 +59,7 @@ pub async fn handle_delete(
|
||||||
};
|
};
|
||||||
let actor_profile = get_profile_by_remote_actor_id(db_client, &activity.actor).await?;
|
let actor_profile = get_profile_by_remote_actor_id(db_client, &activity.actor).await?;
|
||||||
if post.author.id != actor_profile.id {
|
if post.author.id != actor_profile.id {
|
||||||
return Err(ValidationError("actor is not an author").into());
|
return Err(ValidationError("actor is not an author".to_string()).into());
|
||||||
};
|
};
|
||||||
let deletion_queue = delete_post(db_client, &post.id).await?;
|
let deletion_queue = delete_post(db_client, &post.id).await?;
|
||||||
let config = config.clone();
|
let config = config.clone();
|
||||||
|
|
|
@ -31,8 +31,12 @@ pub async fn handle_follow(
|
||||||
activity: Value,
|
activity: Value,
|
||||||
) -> HandlerResult {
|
) -> HandlerResult {
|
||||||
// Follow(Person)
|
// Follow(Person)
|
||||||
let activity: Follow = serde_json::from_value(activity)
|
let activity: Follow = serde_json::from_value(activity.clone()).map_err(|_| {
|
||||||
.map_err(|_| ValidationError("unexpected activity structure"))?;
|
ValidationError(format!(
|
||||||
|
"unexpected Follow activity structure: {}",
|
||||||
|
activity
|
||||||
|
))
|
||||||
|
})?;
|
||||||
let source_profile = get_or_import_profile_by_actor_id(
|
let source_profile = get_or_import_profile_by_actor_id(
|
||||||
db_client,
|
db_client,
|
||||||
&config.instance(),
|
&config.instance(),
|
||||||
|
|
|
@ -30,8 +30,9 @@ pub async fn handle_like(
|
||||||
db_client: &mut impl DatabaseClient,
|
db_client: &mut impl DatabaseClient,
|
||||||
activity: Value,
|
activity: Value,
|
||||||
) -> HandlerResult {
|
) -> HandlerResult {
|
||||||
let activity: Like = serde_json::from_value(activity)
|
let activity: Like = serde_json::from_value(activity.clone()).map_err(|_| {
|
||||||
.map_err(|_| ValidationError("unexpected activity structure"))?;
|
ValidationError(format!("unexpected Like activity structure: {}", activity))
|
||||||
|
})?;
|
||||||
let author = get_or_import_profile_by_actor_id(
|
let author = get_or_import_profile_by_actor_id(
|
||||||
db_client,
|
db_client,
|
||||||
&config.instance(),
|
&config.instance(),
|
||||||
|
|
|
@ -34,12 +34,13 @@ pub async fn handle_move(
|
||||||
activity: Value,
|
activity: Value,
|
||||||
) -> HandlerResult {
|
) -> HandlerResult {
|
||||||
// Move(Person)
|
// Move(Person)
|
||||||
let activity: Move = serde_json::from_value(activity)
|
let activity: Move = serde_json::from_value(activity.clone()).map_err(|_| {
|
||||||
.map_err(|_| ValidationError("unexpected activity structure"))?;
|
ValidationError(format!("unexpected Move activity structure: {}", activity))
|
||||||
|
})?;
|
||||||
// Mastodon: actor is old profile (object)
|
// Mastodon: actor is old profile (object)
|
||||||
// Mitra: actor is new profile (target)
|
// Mitra: actor is new profile (target)
|
||||||
if activity.object != activity.actor && activity.target != activity.actor {
|
if activity.object != activity.actor && activity.target != activity.actor {
|
||||||
return Err(ValidationError("actor ID mismatch").into());
|
return Err(ValidationError("actor ID mismatch".to_string()).into());
|
||||||
};
|
};
|
||||||
|
|
||||||
let instance = config.instance();
|
let instance = config.instance();
|
||||||
|
@ -70,7 +71,7 @@ pub async fn handle_move(
|
||||||
// Add aliases reported by server (actor's alsoKnownAs property)
|
// Add aliases reported by server (actor's alsoKnownAs property)
|
||||||
aliases.extend(new_profile.aliases.clone().into_actor_ids());
|
aliases.extend(new_profile.aliases.clone().into_actor_ids());
|
||||||
if !aliases.contains(&old_actor_id) {
|
if !aliases.contains(&old_actor_id) {
|
||||||
return Err(ValidationError("target ID is not an alias").into());
|
return Err(ValidationError("target ID is not an alias".to_string()).into());
|
||||||
};
|
};
|
||||||
|
|
||||||
let followers = get_followers(db_client, &old_profile.id).await?;
|
let followers = get_followers(db_client, &old_profile.id).await?;
|
||||||
|
|
|
@ -29,13 +29,17 @@ pub async fn handle_reject(
|
||||||
activity: Value,
|
activity: Value,
|
||||||
) -> HandlerResult {
|
) -> HandlerResult {
|
||||||
// Reject(Follow)
|
// Reject(Follow)
|
||||||
let activity: Reject = serde_json::from_value(activity)
|
let activity: Reject = serde_json::from_value(activity.clone()).map_err(|_| {
|
||||||
.map_err(|_| ValidationError("unexpected activity structure"))?;
|
ValidationError(format!(
|
||||||
|
"unexpected Reject activity structure: {}",
|
||||||
|
activity
|
||||||
|
))
|
||||||
|
})?;
|
||||||
let actor_profile = get_profile_by_remote_actor_id(db_client, &activity.actor).await?;
|
let actor_profile = get_profile_by_remote_actor_id(db_client, &activity.actor).await?;
|
||||||
let follow_request_id = parse_local_object_id(&config.instance_url(), &activity.object)?;
|
let follow_request_id = parse_local_object_id(&config.instance_url(), &activity.object)?;
|
||||||
let follow_request = get_follow_request_by_id(db_client, &follow_request_id).await?;
|
let follow_request = get_follow_request_by_id(db_client, &follow_request_id).await?;
|
||||||
if follow_request.target_id != actor_profile.id {
|
if follow_request.target_id != actor_profile.id {
|
||||||
return Err(ValidationError("actor is not a target").into());
|
return Err(ValidationError("actor is not a target".to_string()).into());
|
||||||
};
|
};
|
||||||
if matches!(follow_request.request_status, FollowRequestStatus::Rejected) {
|
if matches!(follow_request.request_status, FollowRequestStatus::Rejected) {
|
||||||
// Ignore Reject if follow request already rejected
|
// Ignore Reject if follow request already rejected
|
||||||
|
|
|
@ -27,8 +27,12 @@ pub async fn handle_remove(
|
||||||
db_client: &mut impl DatabaseClient,
|
db_client: &mut impl DatabaseClient,
|
||||||
activity: Value,
|
activity: Value,
|
||||||
) -> HandlerResult {
|
) -> HandlerResult {
|
||||||
let activity: Remove = serde_json::from_value(activity)
|
let activity: Remove = serde_json::from_value(activity.clone()).map_err(|_| {
|
||||||
.map_err(|_| ValidationError("unexpected activity structure"))?;
|
ValidationError(format!(
|
||||||
|
"unexpected Remove activity structure: {}",
|
||||||
|
activity
|
||||||
|
))
|
||||||
|
})?;
|
||||||
let actor_profile = get_profile_by_remote_actor_id(db_client, &activity.actor).await?;
|
let actor_profile = get_profile_by_remote_actor_id(db_client, &activity.actor).await?;
|
||||||
let actor = actor_profile.actor_json.ok_or(HandlerError::LocalObject)?;
|
let actor = actor_profile.actor_json.ok_or(HandlerError::LocalObject)?;
|
||||||
if Some(activity.target) == actor.subscribers {
|
if Some(activity.target) == actor.subscribers {
|
||||||
|
|
|
@ -30,8 +30,12 @@ async fn handle_undo_follow(
|
||||||
db_client: &mut impl DatabaseClient,
|
db_client: &mut impl DatabaseClient,
|
||||||
activity: Value,
|
activity: Value,
|
||||||
) -> HandlerResult {
|
) -> HandlerResult {
|
||||||
let activity: UndoFollow = serde_json::from_value(activity)
|
let activity: UndoFollow = serde_json::from_value(activity.clone()).map_err(|_| {
|
||||||
.map_err(|_| ValidationError("unexpected activity structure"))?;
|
ValidationError(format!(
|
||||||
|
"unexpected UndoFollow activity structure: {}",
|
||||||
|
activity
|
||||||
|
))
|
||||||
|
})?;
|
||||||
let source_profile = get_profile_by_remote_actor_id(db_client, &activity.actor).await?;
|
let source_profile = get_profile_by_remote_actor_id(db_client, &activity.actor).await?;
|
||||||
let target_actor_id = find_object_id(&activity.object["object"])?;
|
let target_actor_id = find_object_id(&activity.object["object"])?;
|
||||||
let target_username = parse_local_actor_id(&config.instance_url(), &target_actor_id)?;
|
let target_username = parse_local_actor_id(&config.instance_url(), &target_actor_id)?;
|
||||||
|
@ -63,15 +67,16 @@ pub async fn handle_undo(
|
||||||
return handle_undo_follow(config, db_client, activity).await;
|
return handle_undo_follow(config, db_client, activity).await;
|
||||||
};
|
};
|
||||||
|
|
||||||
let activity: Undo = serde_json::from_value(activity)
|
let activity: Undo = serde_json::from_value(activity.clone()).map_err(|_| {
|
||||||
.map_err(|_| ValidationError("unexpected activity structure"))?;
|
ValidationError(format!("unexpected Undo activity structure: {}", activity))
|
||||||
|
})?;
|
||||||
let actor_profile = get_profile_by_remote_actor_id(db_client, &activity.actor).await?;
|
let actor_profile = get_profile_by_remote_actor_id(db_client, &activity.actor).await?;
|
||||||
|
|
||||||
match get_follow_request_by_activity_id(db_client, &activity.object).await {
|
match get_follow_request_by_activity_id(db_client, &activity.object).await {
|
||||||
Ok(follow_request) => {
|
Ok(follow_request) => {
|
||||||
// Undo(Follow)
|
// Undo(Follow)
|
||||||
if follow_request.source_id != actor_profile.id {
|
if follow_request.source_id != actor_profile.id {
|
||||||
return Err(ValidationError("actor is not a follower").into());
|
return Err(ValidationError("actor is not a follower".to_string()).into());
|
||||||
};
|
};
|
||||||
unfollow(
|
unfollow(
|
||||||
db_client,
|
db_client,
|
||||||
|
@ -89,7 +94,7 @@ pub async fn handle_undo(
|
||||||
Ok(reaction) => {
|
Ok(reaction) => {
|
||||||
// Undo(Like)
|
// Undo(Like)
|
||||||
if reaction.author_id != actor_profile.id {
|
if reaction.author_id != actor_profile.id {
|
||||||
return Err(ValidationError("actor is not an author").into());
|
return Err(ValidationError("actor is not an author".to_string()).into());
|
||||||
};
|
};
|
||||||
delete_reaction(db_client, &reaction.author_id, &reaction.post_id).await?;
|
delete_reaction(db_client, &reaction.author_id, &reaction.post_id).await?;
|
||||||
Ok(Some(LIKE))
|
Ok(Some(LIKE))
|
||||||
|
@ -103,13 +108,13 @@ pub async fn handle_undo(
|
||||||
Err(other_error) => return Err(other_error.into()),
|
Err(other_error) => return Err(other_error.into()),
|
||||||
};
|
};
|
||||||
if post.author.id != actor_profile.id {
|
if post.author.id != actor_profile.id {
|
||||||
return Err(ValidationError("actor is not an author").into());
|
return Err(ValidationError("actor is not an author".to_string()).into());
|
||||||
};
|
};
|
||||||
match post.repost_of_id {
|
match post.repost_of_id {
|
||||||
// Ignore returned data because reposts don't have attached files
|
// Ignore returned data because reposts don't have attached files
|
||||||
Some(_) => delete_post(db_client, &post.id).await?,
|
Some(_) => delete_post(db_client, &post.id).await?,
|
||||||
// Can't undo regular post
|
// Can't undo regular post
|
||||||
None => return Err(ValidationError("object is not a repost").into()),
|
None => return Err(ValidationError("object is not a repost".to_string()).into()),
|
||||||
};
|
};
|
||||||
Ok(Some(ANNOUNCE))
|
Ok(Some(ANNOUNCE))
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,8 +38,8 @@ async fn handle_update_note(
|
||||||
db_client: &mut impl DatabaseClient,
|
db_client: &mut impl DatabaseClient,
|
||||||
activity: Value,
|
activity: Value,
|
||||||
) -> HandlerResult {
|
) -> HandlerResult {
|
||||||
let activity: UpdateNote =
|
let activity: UpdateNote = serde_json::from_value(activity.clone())
|
||||||
serde_json::from_value(activity).map_err(|_| ValidationError("invalid object"))?;
|
.map_err(|_| ValidationError(format!("invalid UpdateNote object {activity}")))?;
|
||||||
let object = activity.object;
|
let object = activity.object;
|
||||||
let post = match get_post_by_remote_object_id(db_client, &object.id).await {
|
let post = match get_post_by_remote_object_id(db_client, &object.id).await {
|
||||||
Ok(post) => post,
|
Ok(post) => post,
|
||||||
|
@ -49,7 +49,7 @@ async fn handle_update_note(
|
||||||
};
|
};
|
||||||
let instance = config.instance();
|
let instance = config.instance();
|
||||||
if profile_actor_id(&instance.url(), &post.author) != activity.actor {
|
if profile_actor_id(&instance.url(), &post.author) != activity.actor {
|
||||||
return Err(ValidationError("actor is not an author").into());
|
return Err(ValidationError("actor is not an author".to_string()).into());
|
||||||
};
|
};
|
||||||
let mut content = get_object_content(&object)?;
|
let mut content = get_object_content(&object)?;
|
||||||
if object.object_type != NOTE {
|
if object.object_type != NOTE {
|
||||||
|
@ -65,7 +65,7 @@ async fn handle_update_note(
|
||||||
content += &create_content_link(attachment_url);
|
content += &create_content_link(attachment_url);
|
||||||
}
|
}
|
||||||
if content.is_empty() && attachments.is_empty() {
|
if content.is_empty() && attachments.is_empty() {
|
||||||
return Err(ValidationError("post is empty").into());
|
return Err(ValidationError("post is empty".to_string()).into());
|
||||||
};
|
};
|
||||||
|
|
||||||
let tmdb_api_key = config.tmdb_api_key.clone();
|
let tmdb_api_key = config.tmdb_api_key.clone();
|
||||||
|
@ -106,10 +106,10 @@ async fn handle_update_person(
|
||||||
db_client: &mut impl DatabaseClient,
|
db_client: &mut impl DatabaseClient,
|
||||||
activity: Value,
|
activity: Value,
|
||||||
) -> HandlerResult {
|
) -> HandlerResult {
|
||||||
let activity: UpdatePerson =
|
let activity: UpdatePerson = serde_json::from_value(activity.clone())
|
||||||
serde_json::from_value(activity).map_err(|_| ValidationError("invalid actor data"))?;
|
.map_err(|_| ValidationError(format!("invalid UpdatePerson actor data: {}", activity)))?;
|
||||||
if activity.object.id != activity.actor {
|
if activity.object.id != activity.actor {
|
||||||
return Err(ValidationError("actor ID mismatch").into());
|
return Err(ValidationError("actor ID mismatch".to_string()).into());
|
||||||
};
|
};
|
||||||
let profile = get_profile_by_remote_actor_id(db_client, &activity.object.id).await?;
|
let profile = get_profile_by_remote_actor_id(db_client, &activity.object.id).await?;
|
||||||
update_remote_profile(
|
update_remote_profile(
|
||||||
|
@ -130,7 +130,7 @@ pub async fn handle_update(
|
||||||
) -> HandlerResult {
|
) -> HandlerResult {
|
||||||
let object_type = activity["object"]["type"]
|
let object_type = activity["object"]["type"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or(ValidationError("unknown object type"))?;
|
.ok_or(ValidationError("unknown object type".to_string()))?;
|
||||||
match object_type {
|
match object_type {
|
||||||
NOTE => handle_update_note(config, db_client, activity).await,
|
NOTE => handle_update_note(config, db_client, activity).await,
|
||||||
PERSON => handle_update_person(config, db_client, activity).await,
|
PERSON => handle_update_person(config, db_client, activity).await,
|
||||||
|
|
|
@ -80,7 +80,7 @@ pub fn local_tag_collection(instance_url: &str, tag_name: &str) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_object_id(object_id: &str) -> Result<(), ValidationError> {
|
pub fn validate_object_id(object_id: &str) -> Result<(), ValidationError> {
|
||||||
get_hostname(object_id).map_err(|_| ValidationError("invalid object ID"))?;
|
get_hostname(object_id).map_err(|_| ValidationError("invalid object ID".to_string()))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,13 +89,14 @@ pub fn parse_local_actor_id(instance_url: &str, actor_id: &str) -> Result<String
|
||||||
"^{}/users/(?P<username>[0-9a-zA-Z_]+)$",
|
"^{}/users/(?P<username>[0-9a-zA-Z_]+)$",
|
||||||
instance_url.replace('.', r"\."),
|
instance_url.replace('.', r"\."),
|
||||||
);
|
);
|
||||||
let url_regexp = Regex::new(&url_regexp_str).map_err(|_| ValidationError("error"))?;
|
let url_regexp =
|
||||||
|
Regex::new(&url_regexp_str).map_err(|_| ValidationError("error".to_string()))?;
|
||||||
let url_caps = url_regexp
|
let url_caps = url_regexp
|
||||||
.captures(actor_id)
|
.captures(actor_id)
|
||||||
.ok_or(ValidationError("invalid actor ID"))?;
|
.ok_or(ValidationError("invalid actor ID".to_string()))?;
|
||||||
let username = url_caps
|
let username = url_caps
|
||||||
.name("username")
|
.name("username")
|
||||||
.ok_or(ValidationError("invalid actor ID"))?
|
.ok_or(ValidationError("invalid actor ID".to_string()))?
|
||||||
.as_str()
|
.as_str()
|
||||||
.to_owned();
|
.to_owned();
|
||||||
Ok(username)
|
Ok(username)
|
||||||
|
@ -106,16 +107,17 @@ pub fn parse_local_object_id(instance_url: &str, object_id: &str) -> Result<Uuid
|
||||||
"^{}/objects/(?P<uuid>[0-9a-f-]+)$",
|
"^{}/objects/(?P<uuid>[0-9a-f-]+)$",
|
||||||
instance_url.replace('.', r"\."),
|
instance_url.replace('.', r"\."),
|
||||||
);
|
);
|
||||||
let url_regexp = Regex::new(&url_regexp_str).map_err(|_| ValidationError("error"))?;
|
let url_regexp =
|
||||||
|
Regex::new(&url_regexp_str).map_err(|_| ValidationError("error".to_string()))?;
|
||||||
let url_caps = url_regexp
|
let url_caps = url_regexp
|
||||||
.captures(object_id)
|
.captures(object_id)
|
||||||
.ok_or(ValidationError("invalid object ID"))?;
|
.ok_or(ValidationError("invalid object ID".to_string()))?;
|
||||||
let internal_object_id: Uuid = url_caps
|
let internal_object_id: Uuid = url_caps
|
||||||
.name("uuid")
|
.name("uuid")
|
||||||
.ok_or(ValidationError("invalid object ID"))?
|
.ok_or(ValidationError("invalid object ID".to_string()))?
|
||||||
.as_str()
|
.as_str()
|
||||||
.parse()
|
.parse()
|
||||||
.map_err(|_| ValidationError("invalid object ID"))?;
|
.map_err(|_| ValidationError("invalid object ID".to_string()))?;
|
||||||
Ok(internal_object_id)
|
Ok(internal_object_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -108,7 +108,7 @@ pub fn find_object_id(object: &Value) -> Result<String, ValidationError> {
|
||||||
None => {
|
None => {
|
||||||
let object_id = object["id"]
|
let object_id = object["id"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or(ValidationError("missing object ID"))?
|
.ok_or(ValidationError("missing object ID".to_string()))?
|
||||||
.to_string();
|
.to_string();
|
||||||
object_id
|
object_id
|
||||||
}
|
}
|
||||||
|
@ -133,11 +133,11 @@ pub async fn handle_activity(
|
||||||
) -> Result<(), HandlerError> {
|
) -> Result<(), HandlerError> {
|
||||||
let activity_type = activity["type"]
|
let activity_type = activity["type"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or(ValidationError("type property is missing"))?
|
.ok_or(ValidationError("type property is missing".to_string()))?
|
||||||
.to_owned();
|
.to_owned();
|
||||||
let activity_actor = activity["actor"]
|
let activity_actor = activity["actor"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or(ValidationError("actor property is missing"))?
|
.ok_or(ValidationError("actor property is missing".to_string()))?
|
||||||
.to_owned();
|
.to_owned();
|
||||||
let activity = activity.clone();
|
let activity = activity.clone();
|
||||||
let maybe_object_type = match activity_type.as_str() {
|
let maybe_object_type = match activity_type.as_str() {
|
||||||
|
@ -177,15 +177,15 @@ pub async fn receive_activity(
|
||||||
) -> Result<(), HandlerError> {
|
) -> Result<(), HandlerError> {
|
||||||
let activity_type = activity["type"]
|
let activity_type = activity["type"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or(ValidationError("type property is missing"))?;
|
.ok_or(ValidationError("type property is missing".to_string()))?;
|
||||||
let activity_actor = activity["actor"]
|
let activity_actor = activity["actor"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or(ValidationError("actor property is missing"))?;
|
.ok_or(ValidationError("actor property is missing".to_string()))?;
|
||||||
|
|
||||||
let actor_hostname = url::Url::parse(activity_actor)
|
let actor_hostname = url::Url::parse(activity_actor)
|
||||||
.map_err(|_| ValidationError("invalid actor ID"))?
|
.map_err(|_| ValidationError("invalid actor ID".to_string()))?
|
||||||
.host_str()
|
.host_str()
|
||||||
.ok_or(ValidationError("invalid actor ID"))?
|
.ok_or(ValidationError("invalid actor ID".to_string()))?
|
||||||
.to_string();
|
.to_string();
|
||||||
if config
|
if config
|
||||||
.blocked_instances
|
.blocked_instances
|
||||||
|
@ -295,7 +295,7 @@ pub async fn receive_activity(
|
||||||
|
|
||||||
if activity_type == CREATE {
|
if activity_type == CREATE {
|
||||||
let CreateNote { object, .. } = serde_json::from_value(activity.clone())
|
let CreateNote { object, .. } = serde_json::from_value(activity.clone())
|
||||||
.map_err(|_| ValidationError("invalid object"))?;
|
.map_err(|_| ValidationError(format!("invalid CreateNote object: {}", activity)))?;
|
||||||
if is_unsolicited_message(db_client, &config.instance_url(), &object).await? {
|
if is_unsolicited_message(db_client, &config.instance_url(), &object).await? {
|
||||||
log::warn!("unsolicited message rejected: {}", object.id);
|
log::warn!("unsolicited message rejected: {}", object.id);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|
|
@ -9,7 +9,7 @@ pub fn role_from_str(role_str: &str) -> Result<Role, ValidationError> {
|
||||||
"user" => Role::NormalUser,
|
"user" => Role::NormalUser,
|
||||||
"admin" => Role::Admin,
|
"admin" => Role::Admin,
|
||||||
"read_only_user" => Role::ReadOnlyUser,
|
"read_only_user" => Role::ReadOnlyUser,
|
||||||
_ => return Err(ValidationError("unknown role")),
|
_ => return Err(ValidationError("unknown role".to_string())),
|
||||||
};
|
};
|
||||||
Ok(role)
|
Ok(role)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ pub struct ConversionError;
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
pub struct ValidationError(pub &'static str);
|
pub struct ValidationError(pub String);
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum HttpError {
|
pub enum HttpError {
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
|
|
||||||
|
use crate::activitypub::builders::announce::prepare_announce;
|
||||||
use fedimovies_config::Config;
|
use fedimovies_config::Config;
|
||||||
|
use fedimovies_models::database::DatabaseError;
|
||||||
|
use fedimovies_models::notifications::queries::{delete_notification, get_mention_notifications};
|
||||||
|
use fedimovies_models::posts::queries::create_post;
|
||||||
|
use fedimovies_models::posts::types::PostCreateData;
|
||||||
|
use fedimovies_models::users::queries::get_user_by_id;
|
||||||
use fedimovies_models::{
|
use fedimovies_models::{
|
||||||
database::{get_database_client, DbPool},
|
database::{get_database_client, DbPool},
|
||||||
emojis::queries::{delete_emoji, find_unused_remote_emojis},
|
emojis::queries::{delete_emoji, find_unused_remote_emojis},
|
||||||
posts::queries::{delete_post, find_extraneous_posts},
|
posts::queries::{delete_post, find_extraneous_posts},
|
||||||
profiles::queries::{delete_profile, find_empty_profiles, get_profile_by_id},
|
profiles::queries::{delete_profile, find_empty_profiles, get_profile_by_id},
|
||||||
};
|
};
|
||||||
use fedimovies_models::database::DatabaseError;
|
|
||||||
use fedimovies_models::notifications::queries::{delete_notification, get_mention_notifications};
|
|
||||||
use fedimovies_models::posts::queries::create_post;
|
|
||||||
use fedimovies_models::posts::types::PostCreateData;
|
|
||||||
use fedimovies_models::users::queries::get_user_by_id;
|
|
||||||
use fedimovies_utils::datetime::days_before_now;
|
use fedimovies_utils::datetime::days_before_now;
|
||||||
use crate::activitypub::builders::announce::prepare_announce;
|
|
||||||
|
|
||||||
use crate::activitypub::queues::{
|
use crate::activitypub::queues::{
|
||||||
process_queued_incoming_activities, process_queued_outgoing_activities,
|
process_queued_incoming_activities, process_queued_outgoing_activities,
|
||||||
|
@ -80,7 +80,10 @@ pub async fn prune_remote_emojis(config: &Config, db_pool: &DbPool) -> Result<()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finds mention notifications and repost them
|
// Finds mention notifications and repost them
|
||||||
pub async fn handle_movies_mentions(config: &Config, db_pool: &DbPool) -> Result<(), anyhow::Error> {
|
pub async fn handle_movies_mentions(
|
||||||
|
config: &Config,
|
||||||
|
db_pool: &DbPool,
|
||||||
|
) -> Result<(), anyhow::Error> {
|
||||||
let db_client = &mut **get_database_client(db_pool).await?;
|
let db_client = &mut **get_database_client(db_pool).await?;
|
||||||
log::debug!("Reviewing mentions..");
|
log::debug!("Reviewing mentions..");
|
||||||
// for each mention notification do repost
|
// for each mention notification do repost
|
||||||
|
@ -103,14 +106,20 @@ pub async fn handle_movies_mentions(config: &Config, db_pool: &DbPool) -> Result
|
||||||
}
|
}
|
||||||
let mut post = post_with_mention.clone();
|
let mut post = post_with_mention.clone();
|
||||||
let post_id = post.id;
|
let post_id = post.id;
|
||||||
let current_user = get_user_by_id(&transaction, &mention_notification.recipient.id).await?;
|
let current_user =
|
||||||
|
get_user_by_id(&transaction, &mention_notification.recipient.id).await?;
|
||||||
|
|
||||||
// Repost
|
// Repost
|
||||||
let repost_data = PostCreateData::repost(post.id, None);
|
let repost_data = PostCreateData::repost(post.id, None);
|
||||||
let mut repost = match create_post(&mut transaction, ¤t_user.id, repost_data).await {
|
let mut repost =
|
||||||
|
match create_post(&mut transaction, ¤t_user.id, repost_data).await {
|
||||||
Ok(repost) => repost,
|
Ok(repost) => repost,
|
||||||
Err(DatabaseError::AlreadyExists(err)) => {
|
Err(DatabaseError::AlreadyExists(err)) => {
|
||||||
log::info!("Review as Mention of {} already reposted the post with id {}", current_user.profile.username, post_id);
|
log::info!(
|
||||||
|
"Review as Mention of {} already reposted the post with id {}",
|
||||||
|
current_user.profile.username,
|
||||||
|
post_id
|
||||||
|
);
|
||||||
delete_notification(&mut transaction, mention_notification.id).await?;
|
delete_notification(&mut transaction, mention_notification.id).await?;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -128,7 +137,11 @@ pub async fn handle_movies_mentions(config: &Config, db_pool: &DbPool) -> Result
|
||||||
// Delete notification to avoid re-processing
|
// Delete notification to avoid re-processing
|
||||||
delete_notification(&mut transaction, mention_notification.id).await?;
|
delete_notification(&mut transaction, mention_notification.id).await?;
|
||||||
|
|
||||||
log::info!("Review as Mention of {} reposted with post id {}", current_user.profile.username, post_id);
|
log::info!(
|
||||||
|
"Review as Mention of {} reposted with post id {}",
|
||||||
|
current_user.profile.username,
|
||||||
|
post_id
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(transaction.commit().await?)
|
Ok(transaction.commit().await?)
|
||||||
|
|
|
@ -83,7 +83,9 @@ pub fn run(config: Config, db_pool: DbPool) -> () {
|
||||||
}
|
}
|
||||||
PeriodicTask::PruneRemoteEmojis => prune_remote_emojis(&config, &db_pool).await,
|
PeriodicTask::PruneRemoteEmojis => prune_remote_emojis(&config, &db_pool).await,
|
||||||
PeriodicTask::SubscriptionExpirationMonitor => Ok(()),
|
PeriodicTask::SubscriptionExpirationMonitor => Ok(()),
|
||||||
PeriodicTask::HandleMoviesMentions => handle_movies_mentions(&config, &db_pool).await,
|
PeriodicTask::HandleMoviesMentions => {
|
||||||
|
handle_movies_mentions(&config, &db_pool).await
|
||||||
|
}
|
||||||
};
|
};
|
||||||
task_result.unwrap_or_else(|err| {
|
task_result.unwrap_or_else(|err| {
|
||||||
log::error!("{:?}: {}", task, err);
|
log::error!("{:?}: {}", task, err);
|
||||||
|
|
|
@ -3,6 +3,8 @@ use rsa::RsaPrivateKey;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::json_signatures::proofs::PROOF_TYPE_JCS_RSA;
|
||||||
|
use fedimovies_utils::crypto_rsa::create_rsa_signature;
|
||||||
use fedimovies_utils::{
|
use fedimovies_utils::{
|
||||||
canonicalization::{canonicalize_object, CanonicalizationError},
|
canonicalization::{canonicalize_object, CanonicalizationError},
|
||||||
crypto_rsa::create_rsa_sha256_signature,
|
crypto_rsa::create_rsa_sha256_signature,
|
||||||
|
@ -29,6 +31,19 @@ pub struct IntegrityProof {
|
||||||
pub proof_value: String,
|
pub proof_value: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl IntegrityProof {
|
||||||
|
fn jcs_rsa(signer_key_id: &str, signature: &[u8]) -> Self {
|
||||||
|
Self {
|
||||||
|
proof_type: PROOF_TYPE_JCS_RSA.to_string(),
|
||||||
|
proof_purpose: PROOF_PURPOSE.to_string(),
|
||||||
|
cryptosuite: None,
|
||||||
|
verification_method: signer_key_id.to_string(),
|
||||||
|
created: Utc::now(),
|
||||||
|
proof_value: encode_multibase_base58btc(signature),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum JsonSignatureError {
|
pub enum JsonSignatureError {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
@ -40,16 +55,42 @@ pub enum JsonSignatureError {
|
||||||
#[error("signing error")]
|
#[error("signing error")]
|
||||||
SigningError(#[from] rsa::errors::Error),
|
SigningError(#[from] rsa::errors::Error),
|
||||||
|
|
||||||
#[error("invalid object")]
|
#[error("invalid JSON signature object")]
|
||||||
InvalidObject,
|
InvalidObject,
|
||||||
|
|
||||||
|
#[error("already signed")]
|
||||||
|
AlreadySigned,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_integrity_proof(
|
||||||
|
object_value: &mut Value,
|
||||||
|
proof: IntegrityProof,
|
||||||
|
) -> Result<(), JsonSignatureError> {
|
||||||
|
let object_map = object_value
|
||||||
|
.as_object_mut()
|
||||||
|
.ok_or(JsonSignatureError::InvalidObject)?;
|
||||||
|
if object_map.contains_key(PROOF_KEY) {
|
||||||
|
return Err(JsonSignatureError::AlreadySigned);
|
||||||
|
};
|
||||||
|
let proof_value = serde_json::to_value(proof)?;
|
||||||
|
object_map.insert(PROOF_KEY.to_string(), proof_value);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sign_object(
|
pub fn sign_object(
|
||||||
_object: &Value,
|
object: &Value,
|
||||||
_signer_key: &RsaPrivateKey,
|
signer_key: &RsaPrivateKey,
|
||||||
_signer_key_id: &str,
|
signer_key_id: &str,
|
||||||
) -> Result<Value, JsonSignatureError> {
|
) -> Result<Value, JsonSignatureError> {
|
||||||
Err(JsonSignatureError::InvalidObject)
|
// Canonicalize
|
||||||
|
let message = canonicalize_object(object)?;
|
||||||
|
// Sign
|
||||||
|
let signature = create_rsa_signature(signer_key, &message)?;
|
||||||
|
// Insert proof
|
||||||
|
let proof = IntegrityProof::jcs_rsa(signer_key_id, &signature);
|
||||||
|
let mut object_value = serde_json::to_value(object)?;
|
||||||
|
add_integrity_proof(&mut object_value, proof)?;
|
||||||
|
Ok(object_value)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_object_signed(object: &Value) -> bool {
|
pub fn is_object_signed(object: &Value) -> bool {
|
||||||
|
|
|
@ -9,6 +9,13 @@ pub const PROOF_TYPE_ID_EIP191: &str = "ethereum-eip191-00";
|
||||||
// Identity proof, version 2022A
|
// Identity proof, version 2022A
|
||||||
pub const PROOF_TYPE_ID_MINISIGN: &str = "MitraMinisignSignature2022A";
|
pub const PROOF_TYPE_ID_MINISIGN: &str = "MitraMinisignSignature2022A";
|
||||||
|
|
||||||
|
// Similar to https://identity.foundation/JcsEd25519Signature2020/
|
||||||
|
// - Canonicalization algorithm: JCS
|
||||||
|
// - Digest algorithm: SHA-256
|
||||||
|
// - Signature algorithm: RSASSA-PKCS1-v1_5
|
||||||
|
pub const PROOF_TYPE_JCS_RSA: &str = "MitraJcsRsaSignature2022";
|
||||||
|
pub const PROOF_TYPE_JCS_RSA_LEGACY: &str = "JcsRsaSignature2022";
|
||||||
|
|
||||||
// https://w3c.github.io/vc-data-integrity/#dataintegrityproof
|
// https://w3c.github.io/vc-data-integrity/#dataintegrityproof
|
||||||
pub const DATA_INTEGRITY_PROOF: &str = "DataIntegrityProof";
|
pub const DATA_INTEGRITY_PROOF: &str = "DataIntegrityProof";
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ pub struct SignatureData {
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum JsonSignatureVerificationError {
|
pub enum JsonSignatureVerificationError {
|
||||||
#[error("invalid object")]
|
#[error("invalid signature object")]
|
||||||
InvalidObject,
|
InvalidObject,
|
||||||
|
|
||||||
#[error("no proof")]
|
#[error("no proof")]
|
||||||
|
|
|
@ -299,7 +299,7 @@ impl AccountUpdateData {
|
||||||
) -> Result<ProfileUpdateData, MastodonError> {
|
) -> Result<ProfileUpdateData, MastodonError> {
|
||||||
let maybe_bio = if let Some(ref bio_source) = self.note {
|
let maybe_bio = if let Some(ref bio_source) = self.note {
|
||||||
let bio = markdown_basic_to_html(bio_source)
|
let bio = markdown_basic_to_html(bio_source)
|
||||||
.map_err(|_| ValidationError("invalid markdown"))?;
|
.map_err(|_| ValidationError("invalid markdown".to_string()))?;
|
||||||
Some(bio)
|
Some(bio)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -321,7 +321,7 @@ impl AccountUpdateData {
|
||||||
let mut extra_fields = vec![];
|
let mut extra_fields = vec![];
|
||||||
for field_source in self.fields_attributes.unwrap_or(vec![]) {
|
for field_source in self.fields_attributes.unwrap_or(vec![]) {
|
||||||
let value = markdown_basic_to_html(&field_source.value)
|
let value = markdown_basic_to_html(&field_source.value)
|
||||||
.map_err(|_| ValidationError("invalid markdown"))?;
|
.map_err(|_| ValidationError("invalid markdown".to_string()))?;
|
||||||
let extra_field = ExtraField {
|
let extra_field = ExtraField {
|
||||||
name: field_source.name,
|
name: field_source.name,
|
||||||
value: value,
|
value: value,
|
||||||
|
|
|
@ -72,15 +72,15 @@ pub async fn create_account(
|
||||||
let invite_code = account_data
|
let invite_code = account_data
|
||||||
.invite_code
|
.invite_code
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or(ValidationError("invite code is required"))?;
|
.ok_or(ValidationError("invite code is required".to_string()))?;
|
||||||
if !is_valid_invite_code(db_client, invite_code).await? {
|
if !is_valid_invite_code(db_client, invite_code).await? {
|
||||||
return Err(ValidationError("invalid invite code").into());
|
return Err(ValidationError("invalid invite code".to_string()).into());
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
validate_local_username(&account_data.username)?;
|
validate_local_username(&account_data.username)?;
|
||||||
if account_data.password.is_none() && account_data.message.is_none() {
|
if account_data.password.is_none() && account_data.message.is_none() {
|
||||||
return Err(ValidationError("password or EIP-4361 message is required").into());
|
return Err(ValidationError("password or EIP-4361 message is required".to_string()).into());
|
||||||
};
|
};
|
||||||
let maybe_password_hash = if let Some(password) = account_data.password.as_ref() {
|
let maybe_password_hash = if let Some(password) = account_data.password.as_ref() {
|
||||||
let password_hash = hash_password(password).map_err(|_| MastodonError::InternalError)?;
|
let password_hash = hash_password(password).map_err(|_| MastodonError::InternalError)?;
|
||||||
|
@ -117,7 +117,7 @@ pub async fn create_account(
|
||||||
let user = match create_user(db_client, user_data).await {
|
let user = match create_user(db_client, user_data).await {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(DatabaseError::AlreadyExists(_)) => {
|
Err(DatabaseError::AlreadyExists(_)) => {
|
||||||
return Err(ValidationError("user already exists").into())
|
return Err(ValidationError("user already exists".to_string()).into())
|
||||||
}
|
}
|
||||||
Err(other_error) => return Err(other_error.into()),
|
Err(other_error) => return Err(other_error.into()),
|
||||||
};
|
};
|
||||||
|
@ -280,7 +280,7 @@ async fn search_by_did(
|
||||||
let did: Did = query_params
|
let did: Did = query_params
|
||||||
.did
|
.did
|
||||||
.parse()
|
.parse()
|
||||||
.map_err(|_| ValidationError("invalid DID"))?;
|
.map_err(|_| ValidationError("invalid DID".to_string()))?;
|
||||||
let profiles = search_profiles_by_did(db_client, &did, false).await?;
|
let profiles = search_profiles_by_did(db_client, &did, false).await?;
|
||||||
let base_url = get_request_base_url(connection_info);
|
let base_url = get_request_base_url(connection_info);
|
||||||
let instance_url = config.instance().url();
|
let instance_url = config.instance().url();
|
||||||
|
|
|
@ -16,7 +16,7 @@ impl MarkerQueryParams {
|
||||||
let timeline = match self.timeline.as_ref() {
|
let timeline = match self.timeline.as_ref() {
|
||||||
"home" => Timeline::Home,
|
"home" => Timeline::Home,
|
||||||
"notifications" => Timeline::Notifications,
|
"notifications" => Timeline::Notifications,
|
||||||
_ => return Err(ValidationError("invalid timeline name")),
|
_ => return Err(ValidationError("invalid timeline name".to_string())),
|
||||||
};
|
};
|
||||||
Ok(timeline)
|
Ok(timeline)
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,18 +42,18 @@ async fn authorize_view(
|
||||||
let password_hash = user
|
let password_hash = user
|
||||||
.password_hash
|
.password_hash
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or(ValidationError("password auth is disabled"))?;
|
.ok_or(ValidationError("password auth is disabled".to_string()))?;
|
||||||
let password_correct = verify_password(password_hash, &form_data.password)
|
let password_correct = verify_password(password_hash, &form_data.password)
|
||||||
.map_err(|_| MastodonError::InternalError)?;
|
.map_err(|_| MastodonError::InternalError)?;
|
||||||
if !password_correct {
|
if !password_correct {
|
||||||
return Err(ValidationError("incorrect password").into());
|
return Err(ValidationError("incorrect password".to_string()).into());
|
||||||
};
|
};
|
||||||
if query_params.response_type != "code" {
|
if query_params.response_type != "code" {
|
||||||
return Err(ValidationError("invalid response type").into());
|
return Err(ValidationError("invalid response type".to_string()).into());
|
||||||
};
|
};
|
||||||
let oauth_app = get_oauth_app_by_client_id(db_client, &query_params.client_id).await?;
|
let oauth_app = get_oauth_app_by_client_id(db_client, &query_params.client_id).await?;
|
||||||
if oauth_app.redirect_uri != query_params.redirect_uri {
|
if oauth_app.redirect_uri != query_params.redirect_uri {
|
||||||
return Err(ValidationError("invalid redirect_uri parameter").into());
|
return Err(ValidationError("invalid redirect_uri parameter".to_string()).into());
|
||||||
};
|
};
|
||||||
|
|
||||||
let authorization_code = generate_access_token();
|
let authorization_code = generate_access_token();
|
||||||
|
@ -91,36 +91,35 @@ async fn token_view(
|
||||||
let db_client = &**get_database_client(&db_pool).await?;
|
let db_client = &**get_database_client(&db_pool).await?;
|
||||||
let user = match request_data.grant_type.as_str() {
|
let user = match request_data.grant_type.as_str() {
|
||||||
"authorization_code" => {
|
"authorization_code" => {
|
||||||
let authorization_code = request_data
|
let authorization_code = request_data.code.as_ref().ok_or(ValidationError(
|
||||||
.code
|
"authorization code is required".to_string(),
|
||||||
.as_ref()
|
))?;
|
||||||
.ok_or(ValidationError("authorization code is required"))?;
|
|
||||||
get_user_by_authorization_code(db_client, authorization_code).await?
|
get_user_by_authorization_code(db_client, authorization_code).await?
|
||||||
}
|
}
|
||||||
"password" => {
|
"password" => {
|
||||||
let username = request_data
|
let username = request_data
|
||||||
.username
|
.username
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or(ValidationError("username is required"))?;
|
.ok_or(ValidationError("username is required".to_string()))?;
|
||||||
get_user_by_name(db_client, username).await?
|
get_user_by_name(db_client, username).await?
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(ValidationError("unsupported grant type").into());
|
return Err(ValidationError("unsupported grant type".to_string()).into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if request_data.grant_type == "password" || request_data.grant_type == "ethereum" {
|
if request_data.grant_type == "password" || request_data.grant_type == "ethereum" {
|
||||||
let password = request_data
|
let password = request_data
|
||||||
.password
|
.password
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or(ValidationError("password is required"))?;
|
.ok_or(ValidationError("password is required".to_string()))?;
|
||||||
let password_hash = user
|
let password_hash = user
|
||||||
.password_hash
|
.password_hash
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or(ValidationError("password auth is disabled"))?;
|
.ok_or(ValidationError("password auth is disabled".to_string()))?;
|
||||||
let password_correct =
|
let password_correct =
|
||||||
verify_password(password_hash, password).map_err(|_| MastodonError::InternalError)?;
|
verify_password(password_hash, password).map_err(|_| MastodonError::InternalError)?;
|
||||||
if !password_correct {
|
if !password_correct {
|
||||||
return Err(ValidationError("incorrect password").into());
|
return Err(ValidationError("incorrect password".to_string()).into());
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
let access_token = generate_access_token();
|
let access_token = generate_access_token();
|
||||||
|
|
|
@ -44,10 +44,10 @@ fn parse_profile_query(query: &str) -> Result<(String, Option<String>), Validati
|
||||||
Regex::new(r"^(@|!)?(?P<username>[\w\.-]+)(@(?P<hostname>[\w\.-]+))?$").unwrap();
|
Regex::new(r"^(@|!)?(?P<username>[\w\.-]+)(@(?P<hostname>[\w\.-]+))?$").unwrap();
|
||||||
let acct_query_caps = acct_query_re
|
let acct_query_caps = acct_query_re
|
||||||
.captures(query)
|
.captures(query)
|
||||||
.ok_or(ValidationError("invalid profile query"))?;
|
.ok_or(ValidationError("invalid profile query".to_string()))?;
|
||||||
let username = acct_query_caps
|
let username = acct_query_caps
|
||||||
.name("username")
|
.name("username")
|
||||||
.ok_or(ValidationError("invalid profile query"))?
|
.ok_or(ValidationError("invalid profile query".to_string()))?
|
||||||
.as_str()
|
.as_str()
|
||||||
.to_string();
|
.to_string();
|
||||||
let maybe_hostname = acct_query_caps
|
let maybe_hostname = acct_query_caps
|
||||||
|
@ -60,10 +60,10 @@ fn parse_tag_query(query: &str) -> Result<String, ValidationError> {
|
||||||
let tag_query_re = Regex::new(r"^#(?P<tag>\w+)$").unwrap();
|
let tag_query_re = Regex::new(r"^#(?P<tag>\w+)$").unwrap();
|
||||||
let tag_query_caps = tag_query_re
|
let tag_query_caps = tag_query_re
|
||||||
.captures(query)
|
.captures(query)
|
||||||
.ok_or(ValidationError("invalid tag query"))?;
|
.ok_or(ValidationError("invalid tag query".to_string()))?;
|
||||||
let tag = tag_query_caps
|
let tag = tag_query_caps
|
||||||
.name("tag")
|
.name("tag")
|
||||||
.ok_or(ValidationError("invalid tag query"))?
|
.ok_or(ValidationError("invalid tag query".to_string()))?
|
||||||
.as_str()
|
.as_str()
|
||||||
.to_string();
|
.to_string();
|
||||||
Ok(tag)
|
Ok(tag)
|
||||||
|
|
|
@ -59,7 +59,9 @@ pub fn parse_address_list(csv: &str) -> Result<Vec<ActorAddress>, ValidationErro
|
||||||
addresses.sort();
|
addresses.sort();
|
||||||
addresses.dedup();
|
addresses.dedup();
|
||||||
if addresses.len() > 50 {
|
if addresses.len() > 50 {
|
||||||
return Err(ValidationError("can't process more than 50 items at once"));
|
return Err(ValidationError(
|
||||||
|
"can't process more than 50 items at once".to_string(),
|
||||||
|
));
|
||||||
};
|
};
|
||||||
Ok(addresses)
|
Ok(addresses)
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ async fn client_config_view(
|
||||||
let db_client = &**get_database_client(&db_pool).await?;
|
let db_client = &**get_database_client(&db_pool).await?;
|
||||||
let mut current_user = get_current_user(db_client, auth.token()).await?;
|
let mut current_user = get_current_user(db_client, auth.token()).await?;
|
||||||
if request_data.len() != 1 {
|
if request_data.len() != 1 {
|
||||||
return Err(ValidationError("can't update more than one config").into());
|
return Err(ValidationError("can't update more than one config".to_string()).into());
|
||||||
};
|
};
|
||||||
let (client_name, client_config_value) = request_data
|
let (client_name, client_config_value) = request_data
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -172,11 +172,11 @@ async fn move_followers(
|
||||||
let db_client = &mut **get_database_client(&db_pool).await?;
|
let db_client = &mut **get_database_client(&db_pool).await?;
|
||||||
let current_user = get_current_user(db_client, auth.token()).await?;
|
let current_user = get_current_user(db_client, auth.token()).await?;
|
||||||
if current_user.profile.identity_proofs.inner().is_empty() {
|
if current_user.profile.identity_proofs.inner().is_empty() {
|
||||||
return Err(ValidationError("identity proof is required").into());
|
return Err(ValidationError("identity proof is required".to_string()).into());
|
||||||
};
|
};
|
||||||
let instance = config.instance();
|
let instance = config.instance();
|
||||||
if request_data.from_actor_id.starts_with(&instance.url()) {
|
if request_data.from_actor_id.starts_with(&instance.url()) {
|
||||||
return Err(ValidationError("can't move from local actor").into());
|
return Err(ValidationError("can't move from local actor".to_string()).into());
|
||||||
};
|
};
|
||||||
// Existence of actor is not verified because
|
// Existence of actor is not verified because
|
||||||
// the old profile could have been deleted
|
// the old profile could have been deleted
|
||||||
|
@ -193,7 +193,7 @@ async fn move_followers(
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|profile| profile_actor_id(&instance.url(), &profile));
|
.map(|profile| profile_actor_id(&instance.url(), &profile));
|
||||||
if !aliases.any(|actor_id| actor_id == request_data.from_actor_id) {
|
if !aliases.any(|actor_id| actor_id == request_data.from_actor_id) {
|
||||||
return Err(ValidationError("old profile is not an alias").into());
|
return Err(ValidationError("old profile is not an alias".to_string()).into());
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
let address_list = parse_address_list(&request_data.followers_csv)?;
|
let address_list = parse_address_list(&request_data.followers_csv)?;
|
||||||
|
|
|
@ -57,14 +57,14 @@ async fn create_status(
|
||||||
Some("direct") => Visibility::Direct,
|
Some("direct") => Visibility::Direct,
|
||||||
Some("private") => Visibility::Followers,
|
Some("private") => Visibility::Followers,
|
||||||
Some("subscribers") => Visibility::Subscribers,
|
Some("subscribers") => Visibility::Subscribers,
|
||||||
Some(_) => return Err(ValidationError("invalid visibility parameter").into()),
|
Some(_) => return Err(ValidationError("invalid visibility parameter".to_string()).into()),
|
||||||
None => Visibility::Public,
|
None => Visibility::Public,
|
||||||
};
|
};
|
||||||
let content = match status_data.content_type.as_str() {
|
let content = match status_data.content_type.as_str() {
|
||||||
"text/html" => status_data.status,
|
"text/html" => status_data.status,
|
||||||
"text/markdown" => markdown_lite_to_html(&status_data.status)
|
"text/markdown" => markdown_lite_to_html(&status_data.status)
|
||||||
.map_err(|_| ValidationError("invalid markdown"))?,
|
.map_err(|_| ValidationError("invalid markdown".to_string()))?,
|
||||||
_ => return Err(ValidationError("unsupported content type").into()),
|
_ => return Err(ValidationError("unsupported content type".to_string()).into()),
|
||||||
};
|
};
|
||||||
// Parse content
|
// Parse content
|
||||||
let PostContent {
|
let PostContent {
|
||||||
|
@ -95,21 +95,21 @@ async fn create_status(
|
||||||
mentions.sort();
|
mentions.sort();
|
||||||
mentions.dedup();
|
mentions.dedup();
|
||||||
if mentions.len() > MENTION_LIMIT {
|
if mentions.len() > MENTION_LIMIT {
|
||||||
return Err(ValidationError("too many mentions").into());
|
return Err(ValidationError("too many mentions".to_string()).into());
|
||||||
};
|
};
|
||||||
|
|
||||||
// Links validation
|
// Links validation
|
||||||
if links.len() > 0 && visibility != Visibility::Public {
|
if links.len() > 0 && visibility != Visibility::Public {
|
||||||
return Err(ValidationError("can't add links to non-public posts").into());
|
return Err(ValidationError("can't add links to non-public posts".to_string()).into());
|
||||||
};
|
};
|
||||||
if links.len() > LINK_LIMIT {
|
if links.len() > LINK_LIMIT {
|
||||||
return Err(ValidationError("too many links").into());
|
return Err(ValidationError("too many links".to_string()).into());
|
||||||
};
|
};
|
||||||
|
|
||||||
// Emoji validation
|
// Emoji validation
|
||||||
let emojis: Vec<_> = emojis.iter().map(|emoji| emoji.id).collect();
|
let emojis: Vec<_> = emojis.iter().map(|emoji| emoji.id).collect();
|
||||||
if emojis.len() > EMOJI_LIMIT {
|
if emojis.len() > EMOJI_LIMIT {
|
||||||
return Err(ValidationError("too many emojis").into());
|
return Err(ValidationError("too many emojis".to_string()).into());
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reply validation
|
// Reply validation
|
||||||
|
@ -117,15 +117,15 @@ async fn create_status(
|
||||||
let in_reply_to = match get_post_by_id(db_client, in_reply_to_id).await {
|
let in_reply_to = match get_post_by_id(db_client, in_reply_to_id).await {
|
||||||
Ok(post) => post,
|
Ok(post) => post,
|
||||||
Err(DatabaseError::NotFound(_)) => {
|
Err(DatabaseError::NotFound(_)) => {
|
||||||
return Err(ValidationError("parent post does not exist").into());
|
return Err(ValidationError("parent post does not exist".to_string()).into());
|
||||||
}
|
}
|
||||||
Err(other_error) => return Err(other_error.into()),
|
Err(other_error) => return Err(other_error.into()),
|
||||||
};
|
};
|
||||||
if in_reply_to.repost_of_id.is_some() {
|
if in_reply_to.repost_of_id.is_some() {
|
||||||
return Err(ValidationError("can't reply to repost").into());
|
return Err(ValidationError("can't reply to repost".to_string()).into());
|
||||||
};
|
};
|
||||||
if in_reply_to.visibility != Visibility::Public && visibility != Visibility::Direct {
|
if in_reply_to.visibility != Visibility::Public && visibility != Visibility::Direct {
|
||||||
return Err(ValidationError("reply must have direct visibility").into());
|
return Err(ValidationError("reply must have direct visibility".to_string()).into());
|
||||||
};
|
};
|
||||||
if visibility != Visibility::Public {
|
if visibility != Visibility::Public {
|
||||||
let mut in_reply_to_audience: Vec<_> = in_reply_to
|
let mut in_reply_to_audience: Vec<_> = in_reply_to
|
||||||
|
@ -135,7 +135,7 @@ async fn create_status(
|
||||||
.collect();
|
.collect();
|
||||||
in_reply_to_audience.push(in_reply_to.author.id);
|
in_reply_to_audience.push(in_reply_to.author.id);
|
||||||
if !mentions.iter().all(|id| in_reply_to_audience.contains(id)) {
|
if !mentions.iter().all(|id| in_reply_to_audience.contains(id)) {
|
||||||
return Err(ValidationError("audience can't be expanded").into());
|
return Err(ValidationError("audience can't be expanded".to_string()).into());
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
Some(in_reply_to)
|
Some(in_reply_to)
|
||||||
|
@ -145,7 +145,7 @@ async fn create_status(
|
||||||
// Validate attachments
|
// Validate attachments
|
||||||
let attachments = status_data.media_ids.unwrap_or(vec![]);
|
let attachments = status_data.media_ids.unwrap_or(vec![]);
|
||||||
if attachments.len() > ATTACHMENT_LIMIT {
|
if attachments.len() > ATTACHMENT_LIMIT {
|
||||||
return Err(ValidationError("too many attachments").into());
|
return Err(ValidationError("too many attachments".to_string()).into());
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create post
|
// Create post
|
||||||
|
@ -197,8 +197,8 @@ async fn preview_status(
|
||||||
let content = match status_data.content_type.as_str() {
|
let content = match status_data.content_type.as_str() {
|
||||||
"text/html" => status_data.status,
|
"text/html" => status_data.status,
|
||||||
"text/markdown" => markdown_lite_to_html(&status_data.status)
|
"text/markdown" => markdown_lite_to_html(&status_data.status)
|
||||||
.map_err(|_| ValidationError("invalid markdown"))?,
|
.map_err(|_| ValidationError("invalid markdown".to_string()))?,
|
||||||
_ => return Err(ValidationError("unsupported content type").into()),
|
_ => return Err(ValidationError("unsupported content type".to_string()).into()),
|
||||||
};
|
};
|
||||||
let PostContent {
|
let PostContent {
|
||||||
mut content,
|
mut content,
|
||||||
|
|
|
@ -11,10 +11,10 @@ pub const EMOJI_MEDIA_TYPES: [&str; 4] = ["image/apng", "image/gif", "image/png"
|
||||||
pub fn validate_emoji_name(emoji_name: &str) -> Result<(), ValidationError> {
|
pub fn validate_emoji_name(emoji_name: &str) -> Result<(), ValidationError> {
|
||||||
let name_re = Regex::new(EMOJI_NAME_RE).unwrap();
|
let name_re = Regex::new(EMOJI_NAME_RE).unwrap();
|
||||||
if !name_re.is_match(emoji_name) {
|
if !name_re.is_match(emoji_name) {
|
||||||
return Err(ValidationError("invalid emoji name"));
|
return Err(ValidationError("invalid emoji name".to_string()));
|
||||||
};
|
};
|
||||||
if emoji_name.len() > EMOJI_NAME_SIZE_MAX {
|
if emoji_name.len() > EMOJI_NAME_SIZE_MAX {
|
||||||
return Err(ValidationError("emoji name is too long"));
|
return Err(ValidationError("emoji name is too long".to_string()));
|
||||||
};
|
};
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,12 +23,12 @@ pub fn clean_content(content: &str) -> Result<String, ValidationError> {
|
||||||
// Check content size to not exceed the hard limit
|
// Check content size to not exceed the hard limit
|
||||||
// Character limit from config is not enforced at the backend
|
// Character limit from config is not enforced at the backend
|
||||||
if content.len() > CONTENT_MAX_SIZE {
|
if content.len() > CONTENT_MAX_SIZE {
|
||||||
return Err(ValidationError("post is too long"));
|
return Err(ValidationError("post is too long".to_string()));
|
||||||
};
|
};
|
||||||
let content_safe = clean_html_strict(content, &CONTENT_ALLOWED_TAGS, content_allowed_classes());
|
let content_safe = clean_html_strict(content, &CONTENT_ALLOWED_TAGS, content_allowed_classes());
|
||||||
let content_trimmed = content_safe.trim();
|
let content_trimmed = content_safe.trim();
|
||||||
if content_trimmed.is_empty() {
|
if content_trimmed.is_empty() {
|
||||||
return Err(ValidationError("post can not be empty"));
|
return Err(ValidationError("post can not be empty".to_string()));
|
||||||
};
|
};
|
||||||
Ok(content_trimmed.to_string())
|
Ok(content_trimmed.to_string())
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,21 +16,21 @@ const FIELD_VALUE_MAX_SIZE: usize = 5000;
|
||||||
|
|
||||||
pub fn validate_username(username: &str) -> Result<(), ValidationError> {
|
pub fn validate_username(username: &str) -> Result<(), ValidationError> {
|
||||||
if username.is_empty() {
|
if username.is_empty() {
|
||||||
return Err(ValidationError("username is empty"));
|
return Err(ValidationError("username is empty".to_string()));
|
||||||
};
|
};
|
||||||
if username.len() > 100 {
|
if username.len() > 100 {
|
||||||
return Err(ValidationError("username is too long"));
|
return Err(ValidationError("username is too long".to_string()));
|
||||||
};
|
};
|
||||||
let username_regexp = Regex::new(USERNAME_RE).unwrap();
|
let username_regexp = Regex::new(USERNAME_RE).unwrap();
|
||||||
if !username_regexp.is_match(username) {
|
if !username_regexp.is_match(username) {
|
||||||
return Err(ValidationError("invalid username"));
|
return Err(ValidationError("invalid username".to_string()));
|
||||||
};
|
};
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_display_name(display_name: &str) -> Result<(), ValidationError> {
|
fn validate_display_name(display_name: &str) -> Result<(), ValidationError> {
|
||||||
if display_name.chars().count() > DISPLAY_NAME_MAX_LENGTH {
|
if display_name.chars().count() > DISPLAY_NAME_MAX_LENGTH {
|
||||||
return Err(ValidationError("display name is too long"));
|
return Err(ValidationError("display name is too long".to_string()));
|
||||||
};
|
};
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ fn clean_bio(bio: &str, is_remote: bool) -> Result<String, ValidationError> {
|
||||||
} else {
|
} else {
|
||||||
// Local profile
|
// Local profile
|
||||||
if bio.chars().count() > BIO_MAX_LENGTH {
|
if bio.chars().count() > BIO_MAX_LENGTH {
|
||||||
return Err(ValidationError("bio is too long"));
|
return Err(ValidationError("bio is too long".to_string()));
|
||||||
};
|
};
|
||||||
clean_html_strict(bio, &BIO_ALLOWED_TAGS, vec![])
|
clean_html_strict(bio, &BIO_ALLOWED_TAGS, vec![])
|
||||||
};
|
};
|
||||||
|
@ -63,21 +63,23 @@ fn clean_extra_fields(
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if field.name.len() > FIELD_NAME_MAX_SIZE {
|
if field.name.len() > FIELD_NAME_MAX_SIZE {
|
||||||
return Err(ValidationError("field name is too long"));
|
return Err(ValidationError("field name is too long".to_string()));
|
||||||
};
|
};
|
||||||
if field.value.len() > FIELD_VALUE_MAX_SIZE {
|
if field.value.len() > FIELD_VALUE_MAX_SIZE {
|
||||||
return Err(ValidationError("field value is too long"));
|
return Err(ValidationError("field value is too long".to_string()));
|
||||||
};
|
};
|
||||||
cleaned_extra_fields.push(field);
|
cleaned_extra_fields.push(field);
|
||||||
}
|
}
|
||||||
#[allow(clippy::collapsible_else_if)]
|
#[allow(clippy::collapsible_else_if)]
|
||||||
if is_remote {
|
if is_remote {
|
||||||
if cleaned_extra_fields.len() > 100 {
|
if cleaned_extra_fields.len() > 100 {
|
||||||
return Err(ValidationError("at most 100 fields are allowed"));
|
return Err(ValidationError(
|
||||||
|
"at most 100 fields are allowed".to_string(),
|
||||||
|
));
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
if cleaned_extra_fields.len() > 10 {
|
if cleaned_extra_fields.len() > 10 {
|
||||||
return Err(ValidationError("at most 10 fields are allowed"));
|
return Err(ValidationError("at most 10 fields are allowed".to_string()));
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
Ok(cleaned_extra_fields)
|
Ok(cleaned_extra_fields)
|
||||||
|
@ -88,7 +90,9 @@ pub fn clean_profile_create_data(
|
||||||
) -> Result<(), ValidationError> {
|
) -> Result<(), ValidationError> {
|
||||||
validate_username(&profile_data.username)?;
|
validate_username(&profile_data.username)?;
|
||||||
if profile_data.hostname.is_some() != profile_data.actor_json.is_some() {
|
if profile_data.hostname.is_some() != profile_data.actor_json.is_some() {
|
||||||
return Err(ValidationError("hostname and actor_json field mismatch"));
|
return Err(ValidationError(
|
||||||
|
"hostname and actor_json field mismatch".to_string(),
|
||||||
|
));
|
||||||
};
|
};
|
||||||
if let Some(display_name) = &profile_data.display_name {
|
if let Some(display_name) = &profile_data.display_name {
|
||||||
validate_display_name(display_name)?;
|
validate_display_name(display_name)?;
|
||||||
|
@ -100,7 +104,7 @@ pub fn clean_profile_create_data(
|
||||||
};
|
};
|
||||||
profile_data.extra_fields = clean_extra_fields(&profile_data.extra_fields, is_remote)?;
|
profile_data.extra_fields = clean_extra_fields(&profile_data.extra_fields, is_remote)?;
|
||||||
if profile_data.emojis.len() > EMOJI_LIMIT {
|
if profile_data.emojis.len() > EMOJI_LIMIT {
|
||||||
return Err(ValidationError("too many emojis"));
|
return Err(ValidationError("too many emojis".to_string()));
|
||||||
};
|
};
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -118,7 +122,7 @@ pub fn clean_profile_update_data(
|
||||||
};
|
};
|
||||||
profile_data.extra_fields = clean_extra_fields(&profile_data.extra_fields, is_remote)?;
|
profile_data.extra_fields = clean_extra_fields(&profile_data.extra_fields, is_remote)?;
|
||||||
if profile_data.emojis.len() > EMOJI_LIMIT {
|
if profile_data.emojis.len() > EMOJI_LIMIT {
|
||||||
return Err(ValidationError("too many emojis"));
|
return Err(ValidationError("too many emojis".to_string()));
|
||||||
};
|
};
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ const HASHTAG_NAME_RE: &str = r"^\w+$";
|
||||||
pub fn validate_hashtag(tag_name: &str) -> Result<(), ValidationError> {
|
pub fn validate_hashtag(tag_name: &str) -> Result<(), ValidationError> {
|
||||||
let hashtag_name_re = Regex::new(HASHTAG_NAME_RE).unwrap();
|
let hashtag_name_re = Regex::new(HASHTAG_NAME_RE).unwrap();
|
||||||
if !hashtag_name_re.is_match(tag_name) {
|
if !hashtag_name_re.is_match(tag_name) {
|
||||||
return Err(ValidationError("invalid tag name"));
|
return Err(ValidationError("invalid tag name".to_string()));
|
||||||
};
|
};
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ pub fn validate_local_username(username: &str) -> Result<(), ValidationError> {
|
||||||
// The username regexp should not allow domain names and IP addresses
|
// The username regexp should not allow domain names and IP addresses
|
||||||
let username_regexp = Regex::new(r"^[a-zA-Z0-9_]+$").unwrap();
|
let username_regexp = Regex::new(r"^[a-zA-Z0-9_]+$").unwrap();
|
||||||
if !username_regexp.is_match(username) {
|
if !username_regexp.is_match(username) {
|
||||||
return Err(ValidationError("invalid username"));
|
return Err(ValidationError("invalid username".to_string()));
|
||||||
};
|
};
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,7 @@ impl FromStr for ActorAddress {
|
||||||
let actor_address_re = Regex::new(ACTOR_ADDRESS_RE).unwrap();
|
let actor_address_re = Regex::new(ACTOR_ADDRESS_RE).unwrap();
|
||||||
let caps = actor_address_re
|
let caps = actor_address_re
|
||||||
.captures(value)
|
.captures(value)
|
||||||
.ok_or(ValidationError("invalid actor address"))?;
|
.ok_or(ValidationError("invalid actor address".to_string()))?;
|
||||||
let actor_address = Self {
|
let actor_address = Self {
|
||||||
username: caps["username"].to_string(),
|
username: caps["username"].to_string(),
|
||||||
hostname: caps["hostname"].to_string(),
|
hostname: caps["hostname"].to_string(),
|
||||||
|
|
|
@ -22,7 +22,7 @@ use super::types::{
|
||||||
fn parse_acct_uri(uri: &str) -> Result<ActorAddress, ValidationError> {
|
fn parse_acct_uri(uri: &str) -> Result<ActorAddress, ValidationError> {
|
||||||
let actor_address = uri
|
let actor_address = uri
|
||||||
.strip_prefix("acct:")
|
.strip_prefix("acct:")
|
||||||
.ok_or(ValidationError("invalid query target"))?
|
.ok_or(ValidationError("invalid query target".to_string()))?
|
||||||
.parse()?;
|
.parse()?;
|
||||||
Ok(actor_address)
|
Ok(actor_address)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue