Don't notify when muted

This commit is contained in:
Rafael Caricio 2023-04-27 21:26:18 +02:00
parent cb61f4a86b
commit 22883798b3
Signed by: rafaelcaricio
GPG key ID: 3C86DBCE8E93C947
19 changed files with 104 additions and 314 deletions

View file

@ -1,4 +1,3 @@
use deadpool_postgres::SslMode;
use openssl::ssl::{SslConnector, SslMethod};
use postgres_openssl::MakeTlsConnector;
use std::path::Path;

View file

@ -11,6 +11,7 @@ use crate::notifications::queries::{
create_mention_notification, create_reply_notification, create_repost_notification,
};
use crate::profiles::{queries::update_post_count, types::DbActorProfile};
use crate::relationships::queries::is_muted;
use crate::relationships::types::RelationshipType;
use super::types::{DbPost, Post, PostCreateData, PostUpdateData, Visibility};
@ -249,20 +250,32 @@ pub async fn create_post(
notified_users.push(in_reply_to_author.id);
};
};
// Notify reposted
if let Some(repost_of_id) = &db_post.repost_of_id {
update_repost_count(&transaction, repost_of_id, 1).await?;
let repost_of_author = get_post_author(&transaction, repost_of_id).await?;
if repost_of_author.is_local()
&& repost_of_author.id != db_post.author_id
&& !notified_users.contains(&repost_of_author.id)
// Don't notify themselves that they reported their post
&& repost_of_author.id != db_post.author_id
{
create_repost_notification(
&transaction,
&db_post.author_id,
&repost_of_author.id,
repost_of_id,
)
.await?;
// Don't create mention notification if the author is muted
if is_muted(&transaction, &repost_of_author.id, &db_post.author_id).await? {
log::warn!(
"User {} mentioned by muted author id {} on post id {}, ignoring mention..",
repost_of_author.username,
db_post.author_id,
db_post.id
);
} else {
create_repost_notification(
&transaction,
&db_post.author_id,
&repost_of_author.id,
repost_of_id,
)
.await?;
}
notified_users.push(repost_of_author.id);
};
};
@ -274,8 +287,23 @@ pub async fn create_post(
// or to the author of reposted post
!notified_users.contains(&profile.id)
{
create_mention_notification(&transaction, &db_post.author_id, &profile.id, &db_post.id)
// Don't create mention notification if the author is muted
if is_muted(&transaction, &profile.id, &db_post.author_id).await? {
log::warn!(
"User {} mentioned by muted author {} in post id {}, ignoring mention..",
profile.username,
db_post.author_id,
db_post.id
);
} else {
create_mention_notification(
&transaction,
&db_post.author_id,
&profile.id,
&db_post.id,
)
.await?;
}
};
}
// Construct post object
@ -437,6 +465,7 @@ pub async fn get_home_timeline(
(
post.author_id = $current_user_id
OR (
-- is following or subscribed the post author
EXISTS (
SELECT 1 FROM relationship
WHERE
@ -487,6 +516,14 @@ pub async fn get_home_timeline(
WHERE post_id = post.id AND profile_id = $current_user_id
)
)
-- author is not muted
AND NOT EXISTS (
SELECT 1 FROM relationship
WHERE
source_id = $current_user_id
AND target_id = post.author_id
AND relationship_type = {relationship_mute}
)
AND {visibility_filter}
AND ($max_post_id::uuid IS NULL OR post.id < $max_post_id)
ORDER BY post.id DESC
@ -501,6 +538,7 @@ pub async fn get_home_timeline(
relationship_subscription=i16::from(&RelationshipType::Subscription),
relationship_hide_reposts=i16::from(&RelationshipType::HideReposts),
relationship_hide_replies=i16::from(&RelationshipType::HideReplies),
relationship_mute=i16::from(&RelationshipType::Mute),
visibility_filter=build_visibility_filter(),
);
let limit: i64 = limit.into();

View file

@ -652,6 +652,26 @@ pub async fn unmute_posts(
Ok(())
}
pub async fn is_muted(
db_client: &impl DatabaseClient,
source_id: &Uuid,
target_id: &Uuid,
) -> Result<bool, DatabaseError> {
let rows = db_client
.query(
"
SELECT 1
FROM relationship
WHERE
source_id = $1 AND target_id = $2
AND relationship_type = $3
",
&[&source_id, &target_id, &RelationshipType::Mute],
)
.await?;
Ok(rows.len() > 0)
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -1,14 +1,9 @@
use fedimovies_models::profiles::types::{
ExtraField, IdentityProof, IdentityProofType, PaymentLink, PaymentOption,
};
use fedimovies_utils::did::Did;
use crate::activitypub::vocabulary::{IDENTITY_PROOF, LINK, PROPERTY_VALUE};
use crate::errors::ValidationError;
use crate::identity::{
claims::create_identity_claim,
minisign::{parse_minisign_signature, verify_minisign_signature},
};
use crate::json_signatures::proofs::{PROOF_TYPE_ID_EIP191, PROOF_TYPE_ID_MINISIGN};
use crate::web_client::urls::get_subscription_page_url;
@ -30,51 +25,10 @@ pub fn attach_identity_proof(proof: IdentityProof) -> ActorAttachment {
}
pub fn parse_identity_proof(
actor_id: &str,
attachment: &ActorAttachment,
_actor_id: &str,
_attachment: &ActorAttachment,
) -> Result<IdentityProof, ValidationError> {
if attachment.object_type != IDENTITY_PROOF {
return Err(ValidationError("invalid attachment type".to_string()));
};
let proof_type_str = attachment
.signature_algorithm
.as_ref()
.ok_or(ValidationError("missing proof type".to_string()))?;
let proof_type = match proof_type_str.as_str() {
PROOF_TYPE_ID_EIP191 => IdentityProofType::LegacyEip191IdentityProof,
PROOF_TYPE_ID_MINISIGN => IdentityProofType::LegacyMinisignIdentityProof,
_ => return Err(ValidationError("unsupported proof type".to_string())),
};
let did = attachment
.name
.parse::<Did>()
.map_err(|_| ValidationError("invalid DID".to_string()))?;
let message = create_identity_claim(actor_id, &did)
.map_err(|_| ValidationError("invalid claim".to_string()))?;
let signature = attachment
.signature_value
.as_ref()
.ok_or(ValidationError("missing signature".to_string()))?;
match did {
Did::Key(ref did_key) => {
if !matches!(proof_type, IdentityProofType::LegacyMinisignIdentityProof) {
return Err(ValidationError("incorrect proof type".to_string()));
};
let signature_bin = parse_minisign_signature(signature)
.map_err(|_| ValidationError("invalid signature encoding".to_string()))?;
verify_minisign_signature(did_key, &message, &signature_bin)
.map_err(|_| ValidationError("invalid identity proof".to_string()))?;
}
Did::Pkh(ref _did_pkh) => {
return Err(ValidationError("incorrect proof type".to_string()));
}
};
let proof = IdentityProof {
issuer: did,
proof_type: proof_type,
value: signature.to_string(),
};
Ok(proof)
return Err(ValidationError("incorrect proof type".to_string()));
}
pub fn attach_payment_option(

View file

@ -1,23 +1,9 @@
use std::collections::HashMap;
use serde::{de::Error as DeserializerError, Deserialize, Deserializer, Serialize};
use serde_json::{json, Value};
use fedimovies_config::Instance;
use fedimovies_models::{
profiles::types::{DbActor, DbActorPublicKey, ExtraField, IdentityProof, PaymentOption},
users::types::User,
use super::attachments::{
attach_extra_field, attach_identity_proof, attach_payment_option, parse_extra_field,
parse_identity_proof, parse_payment_option,
};
use fedimovies_utils::{
crypto_rsa::{deserialize_private_key, get_public_key_pem},
urls::get_hostname,
};
use crate::activitypub::types::build_default_context;
use crate::activitypub::{
constants::{
AP_CONTEXT, MASTODON_CONTEXT, MITRA_CONTEXT, SCHEMA_ORG_CONTEXT, W3ID_SECURITY_CONTEXT,
},
identifiers::{
local_actor_id, local_actor_key_id, local_instance_actor_id, LocalActorCollection,
},
@ -27,11 +13,17 @@ use crate::activitypub::{
use crate::errors::ValidationError;
use crate::media::get_file_url;
use crate::webfinger::types::ActorAddress;
use super::attachments::{
attach_extra_field, attach_identity_proof, attach_payment_option, parse_extra_field,
parse_identity_proof, parse_payment_option,
use fedimovies_config::Instance;
use fedimovies_models::{
profiles::types::{DbActor, DbActorPublicKey, ExtraField, IdentityProof, PaymentOption},
users::types::User,
};
use fedimovies_utils::{
crypto_rsa::{deserialize_private_key, get_public_key_pem},
urls::get_hostname,
};
use serde::{de::Error as DeserializerError, Deserialize, Deserializer, Serialize};
use serde_json::{json, Value};
#[derive(Deserialize, Serialize)]
#[cfg_attr(test, derive(Default))]
@ -245,27 +237,6 @@ impl Actor {
pub type ActorKeyError = rsa::pkcs8::Error;
fn build_actor_context() -> (
&'static str,
&'static str,
HashMap<&'static str, &'static str>,
) {
(
AP_CONTEXT,
W3ID_SECURITY_CONTEXT,
HashMap::from([
("manuallyApprovesFollowers", "as:manuallyApprovesFollowers"),
("schema", SCHEMA_ORG_CONTEXT),
("PropertyValue", "schema:PropertyValue"),
("value", "schema:value"),
("toot", MASTODON_CONTEXT),
("IdentityProof", "toot:IdentityProof"),
("fedimovies", MITRA_CONTEXT),
("subscribers", "fedimovies:subscribers"),
]),
)
}
pub fn get_local_actor(user: &User, instance_url: &str) -> Result<Actor, ActorKeyError> {
let username = &user.profile.username;
let actor_id = local_actor_id(instance_url, username);

View file

@ -165,7 +165,7 @@ pub async fn verify_signed_activity(
let signer_key = deserialize_public_key(&signer_actor.public_key.public_key_pem)?;
verify_rsa_json_signature(&signature_data, &signer_key)?;
}
JsonSigner::Did(did) => {
JsonSigner::Did(_did) => {
return Err(AuthenticationError::InvalidJsonSignatureType);
}
};

View file

@ -4,12 +4,11 @@ pub const AP_MEDIA_TYPE: &str =
pub const AS_MEDIA_TYPE: &str = "application/activity+json";
// Contexts
pub const AP_CONTEXT: &str = "https://www.w3.org/ns/activitystreams";
pub const AS_CONTEXT: &str = "https://www.w3.org/ns/activitystreams";
pub const W3ID_SECURITY_CONTEXT: &str = "https://w3id.org/security/v1";
pub const W3ID_DATA_INTEGRITY_CONTEXT: &str = "https://w3id.org/security/data-integrity/v1";
pub const SCHEMA_ORG_CONTEXT: &str = "http://schema.org/";
pub const SCHEMA_ORG_CONTEXT: &str = "http://schema.org/#";
pub const MASTODON_CONTEXT: &str = "http://joinmastodon.org/ns#";
pub const MITRA_CONTEXT: &str = "http://jsonld.fedimovies.social#";
// Misc
pub const AP_PUBLIC: &str = "https://www.w3.org/ns/activitystreams#Public";

View file

@ -9,7 +9,7 @@ use fedimovies_utils::{files::sniff_media_type, urls::guess_protocol};
use crate::activitypub::{
actors::types::Actor,
constants::{AP_CONTEXT, AP_MEDIA_TYPE},
constants::{AP_MEDIA_TYPE, AS_CONTEXT},
http_client::{build_federation_client, get_network_type},
identifiers::{local_actor_key_id, local_instance_actor_id},
types::Object,
@ -153,7 +153,7 @@ pub async fn perform_webfinger_query(
let jrd: JsonResourceDescriptor = serde_json::from_str(&webfinger_data)?;
// Lemmy servers can have Group and Person actors with the same name
// https://github.com/LemmyNet/lemmy/issues/2037
let ap_type_property = format!("{}#type", AP_CONTEXT);
let ap_type_property = format!("{}#type", AS_CONTEXT);
let group_link = jrd.links.iter().find(|link| {
link.rel == "self"
&& link

View file

@ -1,7 +1,6 @@
use std::collections::HashMap;
use chrono::Utc;
use log::warn;
use serde::Deserialize;
use serde_json::Value as JsonValue;
use uuid::Uuid;

View file

@ -1,3 +1,6 @@
use crate::activitypub::constants::{
AS_CONTEXT, MASTODON_CONTEXT, SCHEMA_ORG_CONTEXT, W3ID_SECURITY_CONTEXT,
};
use chrono::{DateTime, Utc};
use serde::{de::Error as DeserializerError, Deserialize, Deserializer, Serialize};
use serde_json::{json, Value};
@ -124,11 +127,11 @@ pub type Context = Value;
pub fn build_default_context() -> Context {
json!([
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
AS_CONTEXT,
W3ID_SECURITY_CONTEXT,
{
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"toot": "http://joinmastodon.org/ns#",
"toot": MASTODON_CONTEXT,
"featured": {
"@id": "toot:featured",
"@type": "@id"
@ -145,7 +148,7 @@ pub fn build_default_context() -> Context {
"@id": "as:movedTo",
"@type": "@id"
},
"schema": "http://schema.org#",
"schema": SCHEMA_ORG_CONTEXT,
"PropertyValue": "schema:PropertyValue",
"value": "schema:value",
"IdentityProof": "toot:IdentityProof",

View file

@ -1,42 +0,0 @@
use serde::Serialize;
use fedimovies_utils::{
canonicalization::{canonicalize_object, CanonicalizationError},
did::Did,
};
// https://www.w3.org/TR/vc-data-model/#credential-subject
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Claim {
id: String, // actor ID
owner_of: Did,
}
/// Creates key ownership claim and prepares it for signing
pub fn create_identity_claim(actor_id: &str, did: &Did) -> Result<String, CanonicalizationError> {
let claim = Claim {
id: actor_id.to_string(),
owner_of: did.clone(),
};
let message = canonicalize_object(&claim)?;
Ok(message)
}
#[cfg(test)]
mod tests {
use super::*;
use fedimovies_utils::{currencies::Currency, did_pkh::DidPkh};
#[test]
fn test_create_identity_claim() {
let actor_id = "https://example.org/users/test";
let ethereum_address = "0xB9C5714089478a327F09197987f16f9E5d936E8a";
let did = Did::Pkh(DidPkh::from_address(&Currency::Ethereum, ethereum_address));
let claim = create_identity_claim(actor_id, &did).unwrap();
assert_eq!(
claim,
r#"{"id":"https://example.org/users/test","ownerOf":"did:pkh:eip155:1:0xb9c5714089478a327f09197987f16f9e5d936e8a"}"#,
);
}
}

View file

@ -1,128 +0,0 @@
/// https://jedisct1.github.io/minisign/
use blake2::{Blake2b512, Digest};
use ed25519_dalek::{PublicKey, Signature, SignatureError, Verifier};
use fedimovies_utils::did_key::{DidKey, MulticodecError};
const MINISIGN_SIGNATURE_CODE: [u8; 2] = *b"Ed";
const MINISIGN_SIGNATURE_HASHED_CODE: [u8; 2] = *b"ED";
#[derive(thiserror::Error, Debug)]
pub enum ParseError {
#[error("invalid encoding")]
InvalidEncoding(#[from] base64::DecodeError),
#[error("invalid key length")]
InvalidKeyLength,
#[error("invalid signature length")]
InvalidSignatureLength,
#[error("invalid signature type")]
InvalidSignatureType,
}
// Public key format:
// base64(<signature_algorithm> || <key_id> || <public_key>)
fn parse_minisign_public_key(key_b64: &str) -> Result<[u8; 32], ParseError> {
let key_bin = base64::decode(key_b64)?;
if key_bin.len() != 42 {
return Err(ParseError::InvalidKeyLength);
};
let mut signature_algorithm = [0; 2];
let mut _key_id = [0; 8];
let mut key = [0; 32];
signature_algorithm.copy_from_slice(&key_bin[0..2]);
_key_id.copy_from_slice(&key_bin[2..10]);
key.copy_from_slice(&key_bin[10..42]);
if signature_algorithm.as_ref() != MINISIGN_SIGNATURE_CODE {
return Err(ParseError::InvalidSignatureType);
};
Ok(key)
}
pub fn minisign_key_to_did(key_b64: &str) -> Result<DidKey, ParseError> {
let key = parse_minisign_public_key(key_b64)?;
let did_key = DidKey::from_ed25519_key(key);
Ok(did_key)
}
// Signature format:
// base64(<signature_algorithm> || <key_id> || <signature>)
pub fn parse_minisign_signature(signature_b64: &str) -> Result<[u8; 64], ParseError> {
let signature_bin = base64::decode(signature_b64)?;
if signature_bin.len() != 74 {
return Err(ParseError::InvalidSignatureLength);
};
let mut signature_algorithm = [0; 2];
let mut _key_id = [0; 8];
let mut signature = [0; 64];
signature_algorithm.copy_from_slice(&signature_bin[0..2]);
_key_id.copy_from_slice(&signature_bin[2..10]);
signature.copy_from_slice(&signature_bin[10..74]);
if signature_algorithm.as_ref() != MINISIGN_SIGNATURE_HASHED_CODE {
return Err(ParseError::InvalidSignatureType);
};
Ok(signature)
}
fn _verify_ed25519_signature(
message: &str,
signer: [u8; 32],
signature: [u8; 64],
) -> Result<(), SignatureError> {
let signature = Signature::from_bytes(&signature)?;
let public_key = PublicKey::from_bytes(&signer)?;
let mut hasher = Blake2b512::new();
hasher.update(message);
let hash = hasher.finalize();
public_key.verify(&hash, &signature)?;
Ok(())
}
#[derive(thiserror::Error, Debug)]
pub enum VerificationError {
#[error(transparent)]
InvalidKey(#[from] MulticodecError),
#[error(transparent)]
ParseError(#[from] ParseError),
#[error(transparent)]
SignatureError(#[from] SignatureError),
}
pub fn verify_minisign_signature(
signer: &DidKey,
message: &str,
signature: &[u8],
) -> Result<(), VerificationError> {
let ed25519_key = signer.try_ed25519_key()?;
let ed25519_signature = signature
.try_into()
.map_err(|_| ParseError::InvalidSignatureLength)?;
let message = format!("{}\n", message);
_verify_ed25519_signature(&message, ed25519_key, ed25519_signature)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_verify_minisign_signature() {
let minisign_key = "RWSA58rRENpGFYwAjRjbdST7VHFoIuH9JBHfO2u6i5JgANPIoQhABAF/";
let message = "test";
let minisign_signature =
"RUSA58rRENpGFVKxdZGMG1WdIJ+dlyP83qOqw6GP0H/Li6Brug2A3mFKLtleIRLi6IIG0smzOlX5CEsisNnc897OUHIOSNLsQQs=";
let signer = minisign_key_to_did(minisign_key).unwrap();
let signature_bin = parse_minisign_signature(minisign_signature).unwrap();
let result = verify_minisign_signature(&signer, message, &signature_bin);
assert_eq!(result.is_ok(), true);
}
}

View file

@ -1,2 +0,0 @@
pub mod claims;
pub mod minisign;

View file

@ -114,7 +114,7 @@ pub async fn handle_movies_mentions(
let mut repost =
match create_post(&mut transaction, &current_user.id, repost_data).await {
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,

View file

@ -7,9 +7,6 @@ use crate::json_signatures::proofs::PROOF_TYPE_JCS_RSA;
use fedimovies_utils::crypto_rsa::create_rsa_signature;
use fedimovies_utils::{
canonicalization::{canonicalize_object, CanonicalizationError},
crypto_rsa::create_rsa_sha256_signature,
did_key::DidKey,
did_pkh::DidPkh,
multibase::encode_multibase_base58btc,
};

View file

@ -14,7 +14,6 @@ pub const PROOF_TYPE_ID_MINISIGN: &str = "MitraMinisignSignature2022A";
// - 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
pub const DATA_INTEGRITY_PROOF: &str = "DataIntegrityProof";
@ -41,7 +40,7 @@ impl FromStr for ProofType {
}
impl ProofType {
pub fn from_cryptosuite(value: &str) -> Result<Self, ConversionError> {
pub fn from_cryptosuite(_value: &str) -> Result<Self, ConversionError> {
Err(ConversionError)
}
}

View file

@ -8,14 +8,11 @@ use fedimovies_utils::{
canonicalization::{canonicalize_object, CanonicalizationError},
crypto_rsa::verify_rsa_sha256_signature,
did::Did,
did_key::DidKey,
did_pkh::DidPkh,
multibase::{decode_multibase_base58btc, MultibaseError},
};
use super::create::{IntegrityProof, PROOF_KEY, PROOF_PURPOSE};
use super::proofs::{ProofType, DATA_INTEGRITY_PROOF};
use crate::identity::minisign::verify_minisign_signature;
#[derive(Debug, PartialEq)]
pub enum JsonSigner {

View file

@ -4,7 +4,6 @@ pub mod atom;
mod errors;
pub mod http;
mod http_signatures;
mod identity;
mod ipfs;
pub mod job_queue;
mod json_signatures;

View file

@ -11,22 +11,18 @@ use fedimovies_models::{
profiles::queries::{
get_profile_by_acct, get_profile_by_id, search_profiles_by_did, update_profile,
},
profiles::types::{IdentityProof, IdentityProofType, ProfileUpdateData},
relationships::queries::{
get_followers_paginated, get_following_paginated, hide_replies, hide_reposts, show_replies,
show_reposts, unfollow,
},
subscriptions::queries::get_incoming_subscriptions,
users::queries::{create_user, get_user_by_did, is_valid_invite_code},
users::queries::{create_user, is_valid_invite_code},
users::types::{Role, UserCreateData},
};
use fedimovies_utils::{
caip2::ChainId,
canonicalization::canonicalize_object,
crypto_rsa::{generate_rsa_key, serialize_private_key},
currencies::Currency,
did::Did,
did_pkh::DidPkh,
id::generate_ulid,
passwords::hash_password,
};
@ -34,25 +30,16 @@ use fedimovies_utils::{
use super::helpers::{get_aliases, get_relationship};
use super::types::{
Account, AccountCreateData, AccountUpdateData, ActivityParams, ApiSubscription, FollowData,
FollowListQueryParams, IdentityClaim, IdentityClaimQueryParams, IdentityProofData,
LookupAcctQueryParams, RelationshipQueryParams, SearchAcctQueryParams, SearchDidQueryParams,
SignedActivity, StatusListQueryParams, UnsignedActivity,
FollowListQueryParams, LookupAcctQueryParams, RelationshipQueryParams, SearchAcctQueryParams,
SearchDidQueryParams, StatusListQueryParams, UnsignedActivity,
};
use crate::activitypub::{
builders::{
follow::follow_or_create_request,
undo_follow::prepare_undo_follow,
update_person::{build_update_person, prepare_update_person},
},
identifiers::local_actor_id,
use crate::activitypub::builders::{
follow::follow_or_create_request,
undo_follow::prepare_undo_follow,
update_person::{build_update_person, prepare_update_person},
};
use crate::errors::ValidationError;
use crate::http::{get_request_base_url, FormOrJson};
use crate::identity::{
claims::create_identity_claim,
minisign::{minisign_key_to_did, parse_minisign_signature, verify_minisign_signature},
};
use crate::json_signatures::create::IntegrityProof;
use crate::mastodon_api::{
errors::MastodonError, oauth::auth::get_current_user, pagination::get_paginated_response,
search::helpers::search_profiles_only, statuses::helpers::build_status_list,