Allow muting accounts #1

Merged
rafaelcaricio merged 5 commits from mute-accounts into main 2023-04-27 20:35:11 +00:00
25 changed files with 250 additions and 341 deletions

30
.woodpecker.yml Normal file
View file

@ -0,0 +1,30 @@
matrix:
RUST: [stable]
pipeline:
check-style:
image: rust
when:
branch: [ master ]
path:
include:
- src/**/*.rs
- fedimovies-*/**/*.rs
commands:
- rustup default $RUST
- cargo clippy --all-targets --all-features -- -D warnings
run-tests:
image: rust
when:
branch: [ master ]
path:
include:
- src/**/*.rs
- fedimovies-*/**/*.rs
environment:
- CARGO_TERM_COLOR=always
commands:
- rustup default $RUST
- cargo test --all -- --nocapture

View file

@ -1,6 +1,7 @@
# Reef
# FediMovies
[![status-badge](https://ci.caric.io/api/badges/FediMovies/fedimovies/status.svg)](https://ci.caric.io/FediMovies/fedimovies)
Lively federated micro-blogging platform.
Lively federated movies reviews platform.
Built on [ActivityPub](https://www.w3.org/TR/activitypub/) protocol, self-hosted, lightweight. Part of the [Fediverse](https://en.wikipedia.org/wiki/Fediverse).
@ -13,8 +14,8 @@ Features:
## Instances
- [FediList](http://demo.fedilist.com/instance?software=reef)
- [Fediverse Observer](https://reef.fediverse.observer/list)
- [FediList](http://demo.fedilist.com/instance?software=fedimovies)
- [Fediverse Observer](https://fedimovies.fediverse.observer/list)
Demo instance: https://nullpointer.social/ ([invite-only](https://nullpointer.social/about))
@ -44,53 +45,53 @@ Run:
cargo build --release --features production
```
This command will produce two binaries in `target/release` directory, `mitra` and `mitractl`.
This command will produce two binaries in `target/release` directory, `fedimovies` and `fedimoviesctl`.
Install PostgreSQL and create the database:
```sql
CREATE USER mitra WITH PASSWORD 'mitra';
CREATE DATABASE mitra OWNER mitra;
CREATE USER fedimovies WITH PASSWORD 'fedimovies';
CREATE DATABASE fedimovies OWNER fedimovies;
```
Create configuration file by copying `contrib/mitra_config.yaml` and configure the instance. Default config file path is `/etc/mitra/config.yaml`, but it can be changed using `CONFIG_PATH` environment variable.
Create configuration file by copying `contrib/fedimovies_config.yaml` and configure the instance. Default config file path is `/etc/fedimovies/config.yaml`, but it can be changed using `CONFIG_PATH` environment variable.
Put any static files into the directory specified in configuration file. Building instructions for `mitra-web` frontend can be found at https://codeberg.org/silverpill/mitra-web#project-setup.
Put any static files into the directory specified in configuration file. Building instructions for `fedimovies-web` frontend can be found at https://code.caric.io/FediMovies/fedimovies#project-setup.
Start Mitra:
Start Fedimovies:
```shell
./mitra
./fedimovies
```
An HTTP server will be needed to handle HTTPS requests. See the example of [nginx configuration file](./contrib/mitra.nginx).
An HTTP server will be needed to handle HTTPS requests. See the example of [nginx configuration file](./contrib/fedimovies.nginx).
To run Mitra as a systemd service, check out the [systemd unit file example](./contrib/mitra.service).
To run Fedimovies as a systemd service, check out the [systemd unit file example](./contrib/fedimovies.service).
### Debian package
Download and install Mitra package:
Download and install Fedimovies package:
```shell
dpkg -i mitra.deb
dpkg -i fedimovies.deb
```
Install PostgreSQL and create the database:
```sql
CREATE USER mitra WITH PASSWORD 'mitra';
CREATE DATABASE mitra OWNER mitra;
CREATE USER fedimovies WITH PASSWORD 'fedimovies';
CREATE DATABASE fedimovies OWNER fedimovies;
```
Open configuration file `/etc/mitra/config.yaml` and configure the instance.
Open configuration file `/etc/fedimovies/config.yaml` and configure the instance.
Start Mitra:
Start Fedimovies:
```shell
systemctl start mitra
systemctl start fedimovies
```
An HTTP server will be needed to handle HTTPS requests. See the example of [nginx configuration file](./contrib/mitra.nginx).
An HTTP server will be needed to handle HTTPS requests. See the example of [nginx configuration file](./contrib/fedimovies.nginx).
### Tor federation
@ -109,7 +110,7 @@ docker-compose up -d
Test connection:
```shell
psql -h localhost -p 55432 -U mitra mitra
psql -h localhost -p 55432 -U fedimovies fedimovies
```
### Run web service
@ -129,7 +130,7 @@ cargo run
### Run CLI
```shell
cargo run --bin mitractl
cargo run --bin fedimoviesctl
```
### Run linter
@ -150,15 +151,15 @@ See [FEDERATION.md](./FEDERATION.md)
## Client API
Most methods are similar to Mastodon API, but Mitra is not fully compatible.
Most methods are similar to Mastodon API, but Fedimovies is not fully compatible.
[OpenAPI spec](./docs/openapi.yaml)
## CLI
`mitractl` is a command-line tool for performing instance maintenance.
`fedimoviesctl` is a command-line tool for performing instance maintenance.
[Documentation](./docs/mitractl.md)
[Documentation](./docs/fedimoviesctl.md)
## License

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,13 +250,24 @@ 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
{
// 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,
@ -263,6 +275,7 @@ pub async fn create_post(
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

@ -614,6 +614,64 @@ pub async fn show_replies(
Ok(())
}
pub async fn mute_posts(
db_client: &impl DatabaseClient,
source_id: &Uuid,
target_id: &Uuid,
) -> Result<(), DatabaseError> {
db_client
.execute(
"
INSERT INTO relationship (source_id, target_id, relationship_type)
VALUES ($1, $2, $3)
ON CONFLICT (source_id, target_id, relationship_type) DO NOTHING
",
&[&source_id, &target_id, &RelationshipType::Mute],
)
.await?;
Ok(())
}
pub async fn unmute_posts(
db_client: &impl DatabaseClient,
source_id: &Uuid,
target_id: &Uuid,
) -> Result<(), DatabaseError> {
// Does not return NotFound error
db_client
.execute(
"
DELETE FROM relationship
WHERE
source_id = $1 AND target_id = $2
AND relationship_type = $3
",
&[&source_id, &target_id, &RelationshipType::Mute],
)
.await?;
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

@ -17,6 +17,7 @@ pub enum RelationshipType {
Subscription,
HideReposts,
HideReplies,
Mute,
}
impl From<&RelationshipType> for i16 {
@ -27,6 +28,7 @@ impl From<&RelationshipType> for i16 {
RelationshipType::Subscription => 3,
RelationshipType::HideReposts => 4,
RelationshipType::HideReplies => 5,
RelationshipType::Mute => 6,
}
}
}
@ -41,6 +43,7 @@ impl TryFrom<i16> for RelationshipType {
3 => Self::Subscription,
4 => Self::HideReposts,
5 => Self::HideReplies,
6 => Self::Mute,
_ => return Err(DatabaseTypeError),
};
Ok(relationship_type)

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)
}
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

@ -52,6 +52,11 @@ pub async fn get_relationship(
relationship_map.showing_replies = false;
};
}
RelationshipType::Mute => {
if relationship.is_direct(source_id, target_id)? {
relationship_map.muting = true;
};
}
};
}
Ok(relationship_map)

View file

@ -402,6 +402,7 @@ pub struct RelationshipMap {
pub subscription_from: bool,
pub showing_reblogs: bool,
pub showing_replies: bool,
pub muting: bool,
}
fn default_showing_reblogs() -> bool {
@ -423,6 +424,7 @@ impl Default for RelationshipMap {
subscription_from: false,
showing_reblogs: default_showing_reblogs(),
showing_replies: default_showing_replies(),
muting: false,
}
}
}

View file

@ -3,6 +3,7 @@ use actix_web_httpauth::extractors::bearer::BearerAuth;
use uuid::Uuid;
use fedimovies_config::{Config, DefaultRole, RegistrationType};
use fedimovies_models::relationships::queries::{mute_posts, unmute_posts};
use fedimovies_models::{
database::{get_database_client, DatabaseError, DbPool},
posts::queries::get_posts_by_author,
@ -10,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,
};
@ -33,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::{
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::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,
@ -367,6 +355,41 @@ async fn unfollow_account(
Ok(HttpResponse::Ok().json(relationship))
}
#[post("/{account_id}/mute")]
async fn mute_account(
auth: BearerAuth,
db_pool: web::Data<DbPool>,
account_id: web::Path<Uuid>,
) -> Result<HttpResponse, MastodonError> {
let db_client = &mut **get_database_client(&db_pool).await?;
let current_user = get_current_user(db_client, auth.token()).await?;
let target = get_profile_by_id(db_client, &account_id).await?;
mute_posts(db_client, &current_user.id, &target.id).await?;
let relationship = get_relationship(db_client, &current_user.id, &target.id).await?;
Ok(HttpResponse::Ok().json(relationship))
}
#[post("/{account_id}/unmute")]
async fn unmute_account(
auth: BearerAuth,
db_pool: web::Data<DbPool>,
account_id: web::Path<Uuid>,
) -> Result<HttpResponse, MastodonError> {
let db_client = &mut **get_database_client(&db_pool).await?;
let current_user = get_current_user(db_client, auth.token()).await?;
let target = get_profile_by_id(db_client, &account_id).await?;
match unmute_posts(db_client, &current_user.id, &target.id).await {
Ok(()) => (),
Err(DatabaseError::NotFound(_)) => (), // not following
Err(other_error) => return Err(other_error.into()),
};
let relationship = get_relationship(db_client, &current_user.id, &target.id).await?;
Ok(HttpResponse::Ok().json(relationship))
}
#[get("/{account_id}/statuses")]
async fn get_account_statuses(
auth: Option<BearerAuth>,
@ -566,6 +589,8 @@ pub fn account_api_scope() -> Scope {
.service(get_account)
.service(follow_account)
.service(unfollow_account)
.service(mute_account)
.service(unmute_account)
.service(get_account_statuses)
.service(get_account_followers)
.service(get_account_following)

View file

@ -18,6 +18,7 @@ struct InstanceStats {
struct InstanceStatusLimits {
max_characters: usize,
max_media_attachments: usize,
characters_reserved_per_url: usize,
}
#[derive(Serialize)]
@ -105,6 +106,7 @@ impl InstanceInfo {
statuses: InstanceStatusLimits {
max_characters: config.limits.posts.character_limit,
max_media_attachments: ATTACHMENT_LIMIT,
characters_reserved_per_url: 32, // not real, but for compatibility
},
media_attachments: InstanceMediaLimits {
supported_mime_types: SUPPORTED_MEDIA_TYPES
@ -117,7 +119,7 @@ impl InstanceInfo {
},
thumbnail: None,
email: "".to_string(),
languages: vec![],
languages: vec!["en".to_string()],
rules: vec![],
urls: None,
login_message: config.login_message.clone(),