Allow muting accounts #1
25 changed files with 250 additions and 341 deletions
30
.woodpecker.yml
Normal file
30
.woodpecker.yml
Normal 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
|
53
README.md
53
README.md
|
@ -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
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
use deadpool_postgres::SslMode;
|
||||
use openssl::ssl::{SslConnector, SslMethod};
|
||||
use postgres_openssl::MakeTlsConnector;
|
||||
use std::path::Path;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"}"#,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
pub mod claims;
|
||||
pub mod minisign;
|
|
@ -114,7 +114,7 @@ pub async fn handle_movies_mentions(
|
|||
let mut repost =
|
||||
match create_post(&mut transaction, ¤t_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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, ¤t_user.id, &target.id).await?;
|
||||
|
||||
let relationship = get_relationship(db_client, ¤t_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, ¤t_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, ¤t_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)
|
||||
|
|
|
@ -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(),
|
||||
|
|
Loading…
Reference in a new issue