Retoot reviews
This commit is contained in:
parent
b049b75873
commit
17d8c11726
26 changed files with 292 additions and 33 deletions
|
@ -5,7 +5,7 @@ description = "Federated micro-blogging platform and content subscription servic
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
|
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.57"
|
rust-version = "1.68"
|
||||||
publish = false
|
publish = false
|
||||||
default-run = "mitra"
|
default-run = "mitra"
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ name = "mitra-cli"
|
||||||
version = "1.22.0"
|
version = "1.22.0"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.57"
|
rust-version = "1.68"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "mitractl"
|
name = "mitractl"
|
||||||
|
|
|
@ -3,7 +3,7 @@ name = "mitra-config"
|
||||||
version = "1.22.0"
|
version = "1.22.0"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.57"
|
rust-version = "1.68"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
mitra-utils = { path = "../mitra-utils" }
|
mitra-utils = { path = "../mitra-utils" }
|
||||||
|
|
|
@ -53,6 +53,11 @@ pub struct Config {
|
||||||
pub instance_short_description: String,
|
pub instance_short_description: String,
|
||||||
pub instance_description: String,
|
pub instance_description: String,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub tmdb_api_key: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub movie_user_password: Option<String>,
|
||||||
|
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub(super) instance_rsa_key: Option<RsaPrivateKey>,
|
pub(super) instance_rsa_key: Option<RsaPrivateKey>,
|
||||||
|
|
||||||
|
@ -99,8 +104,8 @@ impl Config {
|
||||||
onion_proxy_url: self.federation.onion_proxy_url.clone(),
|
onion_proxy_url: self.federation.onion_proxy_url.clone(),
|
||||||
i2p_proxy_url: self.federation.i2p_proxy_url.clone(),
|
i2p_proxy_url: self.federation.i2p_proxy_url.clone(),
|
||||||
// Private instance doesn't send activities and sign requests
|
// Private instance doesn't send activities and sign requests
|
||||||
is_private: !self.federation.enabled
|
is_private: !self.federation.enabled,
|
||||||
|| matches!(self.environment, Environment::Development),
|
// || matches!(self.environment, Environment::Development),
|
||||||
fetcher_timeout: self.federation.fetcher_timeout,
|
fetcher_timeout: self.federation.fetcher_timeout,
|
||||||
deliverer_timeout: self.federation.deliverer_timeout,
|
deliverer_timeout: self.federation.deliverer_timeout,
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ name = "mitra-models"
|
||||||
version = "1.22.0"
|
version = "1.22.0"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.57"
|
rust-version = "1.68"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
mitra-utils = { path = "../mitra-utils" }
|
mitra-utils = { path = "../mitra-utils" }
|
||||||
|
|
|
@ -32,6 +32,22 @@ async fn create_notification(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn delete_notification(
|
||||||
|
db_client: &impl DatabaseClient,
|
||||||
|
notification_id: i32,
|
||||||
|
) -> Result<(), DatabaseError> {
|
||||||
|
db_client
|
||||||
|
.execute(
|
||||||
|
"
|
||||||
|
DELETE FROM notification
|
||||||
|
WHERE id = $1
|
||||||
|
",
|
||||||
|
&[¬ification_id],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn create_follow_notification(
|
pub async fn create_follow_notification(
|
||||||
db_client: &impl DatabaseClient,
|
db_client: &impl DatabaseClient,
|
||||||
sender_id: &Uuid,
|
sender_id: &Uuid,
|
||||||
|
@ -151,7 +167,7 @@ pub async fn get_notifications(
|
||||||
let statement = format!(
|
let statement = format!(
|
||||||
"
|
"
|
||||||
SELECT
|
SELECT
|
||||||
notification, sender, post, post_author,
|
notification, sender, post, post_author, recipient,
|
||||||
{related_attachments},
|
{related_attachments},
|
||||||
{related_mentions},
|
{related_mentions},
|
||||||
{related_tags},
|
{related_tags},
|
||||||
|
@ -164,6 +180,8 @@ pub async fn get_notifications(
|
||||||
ON notification.post_id = post.id
|
ON notification.post_id = post.id
|
||||||
LEFT JOIN actor_profile AS post_author
|
LEFT JOIN actor_profile AS post_author
|
||||||
ON post.author_id = post_author.id
|
ON post.author_id = post_author.id
|
||||||
|
LEFT JOIN actor_profile AS recipient
|
||||||
|
ON notification.recipient_id = recipient.id
|
||||||
WHERE
|
WHERE
|
||||||
recipient_id = $1
|
recipient_id = $1
|
||||||
AND ($2::integer IS NULL OR notification.id < $2)
|
AND ($2::integer IS NULL OR notification.id < $2)
|
||||||
|
@ -202,3 +220,52 @@ pub async fn get_notifications(
|
||||||
.await?;
|
.await?;
|
||||||
Ok(notifications)
|
Ok(notifications)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_mention_notifications(
|
||||||
|
db_client: &impl DatabaseClient,
|
||||||
|
limit: u16,
|
||||||
|
) -> Result<Vec<Notification>, DatabaseError> {
|
||||||
|
let statement = format!(
|
||||||
|
"
|
||||||
|
SELECT
|
||||||
|
notification, sender, post, post_author, recipient,
|
||||||
|
{related_attachments},
|
||||||
|
{related_mentions},
|
||||||
|
{related_tags},
|
||||||
|
{related_links},
|
||||||
|
{related_emojis}
|
||||||
|
FROM notification
|
||||||
|
JOIN actor_profile AS sender
|
||||||
|
ON notification.sender_id = sender.id
|
||||||
|
LEFT JOIN post
|
||||||
|
ON notification.post_id = post.id
|
||||||
|
LEFT JOIN actor_profile AS post_author
|
||||||
|
ON post.author_id = post_author.id
|
||||||
|
LEFT JOIN actor_profile AS recipient
|
||||||
|
ON notification.recipient_id = recipient.id
|
||||||
|
WHERE
|
||||||
|
event_type = $1
|
||||||
|
ORDER BY notification.id DESC
|
||||||
|
LIMIT $2
|
||||||
|
",
|
||||||
|
related_attachments = RELATED_ATTACHMENTS,
|
||||||
|
related_mentions = RELATED_MENTIONS,
|
||||||
|
related_tags = RELATED_TAGS,
|
||||||
|
related_links = RELATED_LINKS,
|
||||||
|
related_emojis = RELATED_EMOJIS,
|
||||||
|
);
|
||||||
|
let rows = db_client
|
||||||
|
.query(
|
||||||
|
&statement,
|
||||||
|
&[
|
||||||
|
&EventType::Mention,
|
||||||
|
&i64::from(limit),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let mut notifications: Vec<Notification> = rows
|
||||||
|
.iter()
|
||||||
|
.map(Notification::try_from)
|
||||||
|
.collect::<Result<_, _>>()?;
|
||||||
|
Ok(notifications)
|
||||||
|
}
|
||||||
|
|
|
@ -82,6 +82,7 @@ struct DbNotification {
|
||||||
pub struct Notification {
|
pub struct Notification {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub sender: DbActorProfile,
|
pub sender: DbActorProfile,
|
||||||
|
pub recipient: DbActorProfile,
|
||||||
pub post: Option<Post>,
|
pub post: Option<Post>,
|
||||||
pub event_type: EventType,
|
pub event_type: EventType,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
|
@ -93,6 +94,7 @@ impl TryFrom<&Row> for Notification {
|
||||||
fn try_from(row: &Row) -> Result<Self, Self::Error> {
|
fn try_from(row: &Row) -> Result<Self, Self::Error> {
|
||||||
let db_notification: DbNotification = row.try_get("notification")?;
|
let db_notification: DbNotification = row.try_get("notification")?;
|
||||||
let db_sender: DbActorProfile = row.try_get("sender")?;
|
let db_sender: DbActorProfile = row.try_get("sender")?;
|
||||||
|
let db_recipient: DbActorProfile = row.try_get("recipient")?;
|
||||||
let maybe_db_post: Option<DbPost> = row.try_get("post")?;
|
let maybe_db_post: Option<DbPost> = row.try_get("post")?;
|
||||||
let maybe_post = match maybe_db_post {
|
let maybe_post = match maybe_db_post {
|
||||||
Some(db_post) => {
|
Some(db_post) => {
|
||||||
|
@ -118,6 +120,7 @@ impl TryFrom<&Row> for Notification {
|
||||||
let notification = Self {
|
let notification = Self {
|
||||||
id: db_notification.id,
|
id: db_notification.id,
|
||||||
sender: db_sender,
|
sender: db_sender,
|
||||||
|
recipient: db_recipient,
|
||||||
post: maybe_post,
|
post: maybe_post,
|
||||||
event_type: db_notification.event_type,
|
event_type: db_notification.event_type,
|
||||||
created_at: db_notification.created_at,
|
created_at: db_notification.created_at,
|
||||||
|
|
|
@ -262,7 +262,7 @@ impl PostCreateData {
|
||||||
pub fn repost(repost_of_id: Uuid, object_id: Option<String>) -> Self {
|
pub fn repost(repost_of_id: Uuid, object_id: Option<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
repost_of_id: Some(repost_of_id),
|
repost_of_id: Some(repost_of_id),
|
||||||
object_id: object_id,
|
object_id,
|
||||||
created_at: Utc::now(),
|
created_at: Utc::now(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
|
|
|
@ -249,7 +249,7 @@ pub async fn get_user_by_name(
|
||||||
"
|
"
|
||||||
SELECT user_account, actor_profile
|
SELECT user_account, actor_profile
|
||||||
FROM user_account JOIN actor_profile USING (id)
|
FROM user_account JOIN actor_profile USING (id)
|
||||||
WHERE actor_profile.username = $1
|
WHERE lower(actor_profile.username) = lower($1)
|
||||||
",
|
",
|
||||||
&[&username],
|
&[&username],
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,7 +3,7 @@ name = "mitra-utils"
|
||||||
version = "1.22.0"
|
version = "1.22.0"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.57"
|
rust-version = "1.68"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Used for HTML sanitization
|
# Used for HTML sanitization
|
||||||
|
|
|
@ -11,7 +11,7 @@ pub fn get_hostname(url: &str) -> Result<String, ParseError> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn guess_protocol(hostname: &str) -> &'static str {
|
pub fn guess_protocol(hostname: &str) -> &'static str {
|
||||||
if hostname == "localhost" || hostname.ends_with(".example.com") {
|
if hostname == "localhost" {
|
||||||
return "http";
|
return "http";
|
||||||
};
|
};
|
||||||
let maybe_ipv4_address = hostname.parse::<Ipv4Addr>();
|
let maybe_ipv4_address = hostname.parse::<Ipv4Addr>();
|
||||||
|
|
|
@ -109,6 +109,8 @@ pub struct Actor {
|
||||||
pub inbox: String,
|
pub inbox: String,
|
||||||
pub outbox: String,
|
pub outbox: String,
|
||||||
|
|
||||||
|
pub bot: bool,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub followers: Option<String>,
|
pub followers: Option<String>,
|
||||||
|
|
||||||
|
@ -325,6 +327,7 @@ pub fn get_local_actor(user: &User, instance_url: &str) -> Result<Actor, ActorKe
|
||||||
preferred_username: username.to_string(),
|
preferred_username: username.to_string(),
|
||||||
inbox,
|
inbox,
|
||||||
outbox,
|
outbox,
|
||||||
|
bot: true,
|
||||||
followers: Some(followers),
|
followers: Some(followers),
|
||||||
following: Some(following),
|
following: Some(following),
|
||||||
subscribers: Some(subscribers),
|
subscribers: Some(subscribers),
|
||||||
|
@ -359,6 +362,7 @@ pub fn get_instance_actor(instance: &Instance) -> Result<Actor, ActorKeyError> {
|
||||||
preferred_username: instance.hostname(),
|
preferred_username: instance.hostname(),
|
||||||
inbox: actor_inbox,
|
inbox: actor_inbox,
|
||||||
outbox: actor_outbox,
|
outbox: actor_outbox,
|
||||||
|
bot: true,
|
||||||
followers: None,
|
followers: None,
|
||||||
following: None,
|
following: None,
|
||||||
subscribers: None,
|
subscribers: None,
|
||||||
|
|
|
@ -153,6 +153,8 @@ pub async fn import_post(
|
||||||
db_client: &mut impl DatabaseClient,
|
db_client: &mut impl DatabaseClient,
|
||||||
instance: &Instance,
|
instance: &Instance,
|
||||||
storage: &MediaStorage,
|
storage: &MediaStorage,
|
||||||
|
tmdb_api_key: Option<String>,
|
||||||
|
default_movie_user_password: Option<String>,
|
||||||
object_id: String,
|
object_id: String,
|
||||||
object_received: Option<Object>,
|
object_received: Option<Object>,
|
||||||
) -> Result<Post, HandlerError> {
|
) -> Result<Post, HandlerError> {
|
||||||
|
@ -245,7 +247,16 @@ pub async fn import_post(
|
||||||
// starting with the root
|
// starting with the root
|
||||||
objects.reverse();
|
objects.reverse();
|
||||||
for object in objects {
|
for object in objects {
|
||||||
let post = handle_note(db_client, instance, storage, object, &redirects).await?;
|
let post = handle_note(
|
||||||
|
db_client,
|
||||||
|
instance,
|
||||||
|
storage,
|
||||||
|
tmdb_api_key.clone(),
|
||||||
|
default_movie_user_password.clone(),
|
||||||
|
object,
|
||||||
|
&redirects,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
posts.push(post);
|
posts.push(post);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,18 @@ pub async fn handle_announce(
|
||||||
Ok(post_id) => post_id,
|
Ok(post_id) => post_id,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// Try to get remote post
|
// Try to get remote post
|
||||||
let post = import_post(db_client, &instance, &storage, activity.object, None).await?;
|
let tmdb_api_key = config.tmdb_api_key.clone();
|
||||||
|
let default_movie_user_password = config.movie_user_password.clone();
|
||||||
|
let post = import_post(
|
||||||
|
db_client,
|
||||||
|
&instance,
|
||||||
|
&storage,
|
||||||
|
tmdb_api_key,
|
||||||
|
default_movie_user_password,
|
||||||
|
activity.object,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
post.id
|
post.id
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -36,6 +36,7 @@ use crate::activitypub::{
|
||||||
};
|
};
|
||||||
use crate::errors::ValidationError;
|
use crate::errors::ValidationError;
|
||||||
use crate::media::MediaStorage;
|
use crate::media::MediaStorage;
|
||||||
|
use crate::tmdb::lookup_and_create_movie_user;
|
||||||
use crate::validators::{
|
use crate::validators::{
|
||||||
emojis::{validate_emoji_name, EMOJI_MEDIA_TYPES},
|
emojis::{validate_emoji_name, EMOJI_MEDIA_TYPES},
|
||||||
posts::{
|
posts::{
|
||||||
|
@ -306,6 +307,8 @@ pub async fn get_object_tags(
|
||||||
db_client: &mut impl DatabaseClient,
|
db_client: &mut impl DatabaseClient,
|
||||||
instance: &Instance,
|
instance: &Instance,
|
||||||
storage: &MediaStorage,
|
storage: &MediaStorage,
|
||||||
|
api_key: Option<String>,
|
||||||
|
default_movie_user_password: Option<String>,
|
||||||
object: &Object,
|
object: &Object,
|
||||||
redirects: &HashMap<String, String>,
|
redirects: &HashMap<String, String>,
|
||||||
) -> Result<(Vec<Uuid>, Vec<String>, Vec<Uuid>, Vec<Uuid>), HandlerError> {
|
) -> Result<(Vec<Uuid>, Vec<String>, Vec<Uuid>, Vec<Uuid>), HandlerError> {
|
||||||
|
@ -346,7 +349,33 @@ pub async fn get_object_tags(
|
||||||
// Try to find profile by actor ID.
|
// Try to find profile by actor ID.
|
||||||
if let Some(href) = tag.href {
|
if let Some(href) = tag.href {
|
||||||
if let Ok(username) = parse_local_actor_id(&instance.url(), &href) {
|
if let Ok(username) = parse_local_actor_id(&instance.url(), &href) {
|
||||||
let user = get_user_by_name(db_client, &username).await?;
|
// 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 {
|
||||||
|
Ok(user) => {
|
||||||
|
user
|
||||||
|
},
|
||||||
|
Err(DatabaseError::NotFound(_)) => {
|
||||||
|
if let Some(api_key) = &api_key {
|
||||||
|
log::warn!("failed to find mentioned user by name {}, checking if its a valid movie...", username);
|
||||||
|
lookup_and_create_movie_user(
|
||||||
|
instance,
|
||||||
|
db_client,
|
||||||
|
api_key,
|
||||||
|
&storage.media_dir,
|
||||||
|
&username,
|
||||||
|
default_movie_user_password.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
log::warn!("failed to create movie user {username}: {err}");
|
||||||
|
HandlerError::LocalObject
|
||||||
|
})?
|
||||||
|
} else {
|
||||||
|
return Err(HandlerError::LocalObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => return Err(error.into()),
|
||||||
|
};
|
||||||
if !mentions.contains(&user.id) {
|
if !mentions.contains(&user.id) {
|
||||||
mentions.push(user.id);
|
mentions.push(user.id);
|
||||||
};
|
};
|
||||||
|
@ -503,6 +532,8 @@ pub async fn handle_note(
|
||||||
db_client: &mut impl DatabaseClient,
|
db_client: &mut impl DatabaseClient,
|
||||||
instance: &Instance,
|
instance: &Instance,
|
||||||
storage: &MediaStorage,
|
storage: &MediaStorage,
|
||||||
|
tmdb_api_key: Option<String>,
|
||||||
|
default_movie_user_password: Option<String>,
|
||||||
object: Object,
|
object: Object,
|
||||||
redirects: &HashMap<String, String>,
|
redirects: &HashMap<String, String>,
|
||||||
) -> Result<Post, HandlerError> {
|
) -> Result<Post, HandlerError> {
|
||||||
|
@ -543,8 +574,16 @@ pub async fn handle_note(
|
||||||
return Err(ValidationError("post is empty").into());
|
return Err(ValidationError("post is empty").into());
|
||||||
};
|
};
|
||||||
|
|
||||||
let (mentions, hashtags, links, emojis) =
|
let (mentions, hashtags, links, emojis) = get_object_tags(
|
||||||
get_object_tags(db_client, instance, storage, &object, redirects).await?;
|
db_client,
|
||||||
|
instance,
|
||||||
|
storage,
|
||||||
|
tmdb_api_key,
|
||||||
|
default_movie_user_password,
|
||||||
|
&object,
|
||||||
|
redirects,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let in_reply_to_id = match object.in_reply_to {
|
let in_reply_to_id = match object.in_reply_to {
|
||||||
Some(ref object_id) => {
|
Some(ref object_id) => {
|
||||||
|
@ -632,10 +671,15 @@ pub async fn handle_create(
|
||||||
// Most likely it's a forwarded reply.
|
// Most likely it's a forwarded reply.
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let tmdb_api_key = config.tmdb_api_key.clone();
|
||||||
|
let default_movie_user_password = config.movie_user_password.clone();
|
||||||
import_post(
|
import_post(
|
||||||
db_client,
|
db_client,
|
||||||
&config.instance(),
|
&config.instance(),
|
||||||
&MediaStorage::from(config),
|
&MediaStorage::from(config),
|
||||||
|
tmdb_api_key,
|
||||||
|
default_movie_user_password,
|
||||||
object_id,
|
object_id,
|
||||||
object_received,
|
object_received,
|
||||||
)
|
)
|
||||||
|
|
|
@ -67,8 +67,19 @@ async fn handle_update_note(
|
||||||
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").into());
|
||||||
};
|
};
|
||||||
let (mentions, hashtags, links, emojis) =
|
|
||||||
get_object_tags(db_client, &instance, &storage, &object, &HashMap::new()).await?;
|
let tmdb_api_key = config.tmdb_api_key.clone();
|
||||||
|
let default_movie_user_password = config.movie_user_password.clone();
|
||||||
|
let (mentions, hashtags, links, emojis) = get_object_tags(
|
||||||
|
db_client,
|
||||||
|
&instance,
|
||||||
|
&storage,
|
||||||
|
tmdb_api_key,
|
||||||
|
default_movie_user_password,
|
||||||
|
&object,
|
||||||
|
&HashMap::new(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
let updated_at = object.updated.unwrap_or(Utc::now());
|
let updated_at = object.updated.unwrap_or(Utc::now());
|
||||||
let post_data = PostUpdateData {
|
let post_data = PostUpdateData {
|
||||||
content,
|
content,
|
||||||
|
|
|
@ -86,7 +86,7 @@ pub fn validate_object_id(object_id: &str) -> Result<(), ValidationError> {
|
||||||
|
|
||||||
pub fn parse_local_actor_id(instance_url: &str, actor_id: &str) -> Result<String, ValidationError> {
|
pub fn parse_local_actor_id(instance_url: &str, actor_id: &str) -> Result<String, ValidationError> {
|
||||||
let url_regexp_str = format!(
|
let url_regexp_str = format!(
|
||||||
"^{}/users/(?P<username>[0-9a-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"))?;
|
||||||
|
|
|
@ -7,7 +7,13 @@ use mitra_models::{
|
||||||
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 mitra_models::database::DatabaseError;
|
||||||
|
use mitra_models::notifications::queries::{delete_notification, get_mention_notifications};
|
||||||
|
use mitra_models::posts::queries::create_post;
|
||||||
|
use mitra_models::posts::types::PostCreateData;
|
||||||
|
use mitra_models::users::queries::get_user_by_id;
|
||||||
use mitra_utils::datetime::days_before_now;
|
use mitra_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,
|
||||||
|
@ -72,3 +78,58 @@ pub async fn prune_remote_emojis(config: &Config, db_pool: &DbPool) -> Result<()
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Finds mention notifications and repost them
|
||||||
|
pub async fn handle_movies_mentions(config: &Config, db_pool: &DbPool) -> Result<(), anyhow::Error> {
|
||||||
|
let db_client = &mut **get_database_client(db_pool).await?;
|
||||||
|
log::debug!("Reviewing mentions..");
|
||||||
|
// for each mention notification do repost
|
||||||
|
let mut transaction = db_client.transaction().await?;
|
||||||
|
|
||||||
|
let mention_notifications = match get_mention_notifications(&transaction, 50).await {
|
||||||
|
Ok(mention_notifications) => mention_notifications,
|
||||||
|
Err(DatabaseError::DatabaseClientError(err)) => {
|
||||||
|
return Err(anyhow::anyhow!("Error in client: {err}"))
|
||||||
|
}
|
||||||
|
Err(err) => return Err(err.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
for mention_notification in mention_notifications {
|
||||||
|
log::info!("Reviewing mention notification {}", mention_notification.id);
|
||||||
|
if let Some(post_with_mention) = mention_notification.post {
|
||||||
|
// Does not repost private posts or reposts
|
||||||
|
if !post_with_mention.is_public() || post_with_mention.repost_of_id.is_some() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut post = post_with_mention.clone();
|
||||||
|
let post_id = post.id;
|
||||||
|
let current_user = get_user_by_id(&transaction, &mention_notification.recipient.id).await?;
|
||||||
|
|
||||||
|
// Repost
|
||||||
|
let repost_data = PostCreateData::repost(post.id, None);
|
||||||
|
let mut repost = match create_post(&mut transaction, ¤t_user.id, repost_data).await {
|
||||||
|
Ok(repost) => repost,
|
||||||
|
Err(DatabaseError::AlreadyExists(err)) => {
|
||||||
|
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?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(err) => return Err(err.into()),
|
||||||
|
};
|
||||||
|
post.repost_count += 1;
|
||||||
|
repost.repost_of = Some(Box::new(post));
|
||||||
|
|
||||||
|
// Federate
|
||||||
|
prepare_announce(&transaction, &config.instance(), ¤t_user, &repost)
|
||||||
|
.await?
|
||||||
|
.enqueue(&mut transaction)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Delete notification to avoid re-processing
|
||||||
|
delete_notification(&mut transaction, mention_notification.id).await?;
|
||||||
|
|
||||||
|
log::info!("Review as Mention of {} reposted with post id {}", current_user.profile.username, post_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(transaction.commit().await?)
|
||||||
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ enum PeriodicTask {
|
||||||
DeleteExtraneousPosts,
|
DeleteExtraneousPosts,
|
||||||
DeleteEmptyProfiles,
|
DeleteEmptyProfiles,
|
||||||
PruneRemoteEmojis,
|
PruneRemoteEmojis,
|
||||||
|
HandleMoviesMentions,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PeriodicTask {
|
impl PeriodicTask {
|
||||||
|
@ -28,6 +29,7 @@ impl PeriodicTask {
|
||||||
Self::DeleteExtraneousPosts => 3600,
|
Self::DeleteExtraneousPosts => 3600,
|
||||||
Self::DeleteEmptyProfiles => 3600,
|
Self::DeleteEmptyProfiles => 3600,
|
||||||
Self::PruneRemoteEmojis => 3600,
|
Self::PruneRemoteEmojis => 3600,
|
||||||
|
Self::HandleMoviesMentions => 5,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,6 +51,7 @@ pub fn run(config: Config, db_pool: DbPool) -> () {
|
||||||
(PeriodicTask::IncomingActivityQueueExecutor, None),
|
(PeriodicTask::IncomingActivityQueueExecutor, None),
|
||||||
(PeriodicTask::OutgoingActivityQueueExecutor, None),
|
(PeriodicTask::OutgoingActivityQueueExecutor, None),
|
||||||
(PeriodicTask::PruneRemoteEmojis, None),
|
(PeriodicTask::PruneRemoteEmojis, None),
|
||||||
|
(PeriodicTask::HandleMoviesMentions, None),
|
||||||
]);
|
]);
|
||||||
if config.retention.extraneous_posts.is_some() {
|
if config.retention.extraneous_posts.is_some() {
|
||||||
scheduler_state.insert(PeriodicTask::DeleteExtraneousPosts, None);
|
scheduler_state.insert(PeriodicTask::DeleteExtraneousPosts, None);
|
||||||
|
@ -80,6 +83,7 @@ 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,
|
||||||
};
|
};
|
||||||
task_result.unwrap_or_else(|err| {
|
task_result.unwrap_or_else(|err| {
|
||||||
log::error!("{:?}: {}", task, err);
|
log::error!("{:?}: {}", task, err);
|
||||||
|
|
|
@ -12,6 +12,7 @@ pub mod logger;
|
||||||
pub mod mastodon_api;
|
pub mod mastodon_api;
|
||||||
pub mod media;
|
pub mod media;
|
||||||
pub mod nodeinfo;
|
pub mod nodeinfo;
|
||||||
|
mod tmdb;
|
||||||
pub mod validators;
|
pub mod validators;
|
||||||
pub mod web_client;
|
pub mod web_client;
|
||||||
pub mod webfinger;
|
pub mod webfinger;
|
||||||
|
|
|
@ -155,7 +155,19 @@ async fn find_post_by_url(
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
instance.fetcher_timeout = SEARCH_FETCHER_TIMEOUT;
|
instance.fetcher_timeout = SEARCH_FETCHER_TIMEOUT;
|
||||||
match import_post(db_client, &instance, &storage, url.to_string(), None).await {
|
let tmdb_api_key = config.tmdb_api_key.clone();
|
||||||
|
let default_movie_user_password = config.movie_user_password.clone();
|
||||||
|
match import_post(
|
||||||
|
db_client,
|
||||||
|
&instance,
|
||||||
|
&storage,
|
||||||
|
tmdb_api_key,
|
||||||
|
default_movie_user_password,
|
||||||
|
url.to_string(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(post) => Some(post),
|
Ok(post) => Some(post),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("{}", err);
|
log::warn!("{}", err);
|
||||||
|
|
|
@ -15,7 +15,7 @@ use crate::webfinger::types::ActorAddress;
|
||||||
// See also: ACTOR_ADDRESS_RE in webfinger::types
|
// See also: ACTOR_ADDRESS_RE in webfinger::types
|
||||||
const MENTION_SEARCH_RE: &str = r"(?m)(?P<before>^|\s|>|[\(])@(?P<mention>[^\s<]+)";
|
const MENTION_SEARCH_RE: &str = r"(?m)(?P<before>^|\s|>|[\(])@(?P<mention>[^\s<]+)";
|
||||||
const MENTION_SEARCH_SECONDARY_RE: &str =
|
const MENTION_SEARCH_SECONDARY_RE: &str =
|
||||||
r"^(?P<username>[\w\.-]+)(@(?P<hostname>[\w\.-]+\w))?(?P<after>[\.,:?!\)]?)$";
|
r"^(?P<username>[\w\.-_]+)(@(?P<hostname>[\w\.-]+\w))?(?P<after>[\.,:?!\)]?)$";
|
||||||
|
|
||||||
/// Finds everything that looks like a mention
|
/// Finds everything that looks like a mention
|
||||||
fn find_mentions(instance_hostname: &str, text: &str) -> Vec<String> {
|
fn find_mentions(instance_hostname: &str, text: &str) -> Vec<String> {
|
||||||
|
|
|
@ -28,11 +28,8 @@ pub async fn authorize_subscription(
|
||||||
) -> Result<HttpResponse, MastodonError> {
|
) -> Result<HttpResponse, MastodonError> {
|
||||||
let db_client = &**get_database_client(&db_pool).await?;
|
let db_client = &**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?;
|
||||||
|
// We don't have subscriptions
|
||||||
// The user must have a public ethereum address,
|
Err(MastodonError::PermissionError)
|
||||||
// because subscribers should be able
|
|
||||||
// to verify that payments are actually sent to the recipient.
|
|
||||||
return Err(MastodonError::PermissionError);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/options")]
|
#[get("/options")]
|
||||||
|
|
|
@ -7,7 +7,7 @@ use super::profiles::validate_username;
|
||||||
pub fn validate_local_username(username: &str) -> Result<(), ValidationError> {
|
pub fn validate_local_username(username: &str) -> Result<(), ValidationError> {
|
||||||
validate_username(username)?;
|
validate_username(username)?;
|
||||||
// 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-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"));
|
||||||
};
|
};
|
||||||
|
|
|
@ -135,9 +135,9 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_actor_address_parse_address() {
|
fn test_actor_address_parse_address() {
|
||||||
let value = "user_1@example.com";
|
let value = "Matrix_1999@example.com";
|
||||||
let actor_address: ActorAddress = value.parse().unwrap();
|
let actor_address: ActorAddress = value.parse().unwrap();
|
||||||
assert_eq!(actor_address.username, "user_1");
|
assert_eq!(actor_address.username, "Matrix_1999");
|
||||||
assert_eq!(actor_address.hostname, "example.com");
|
assert_eq!(actor_address.hostname, "example.com");
|
||||||
assert_eq!(actor_address.to_string(), value);
|
assert_eq!(actor_address.to_string(), value);
|
||||||
}
|
}
|
||||||
|
@ -146,7 +146,7 @@ mod tests {
|
||||||
fn test_actor_address_parse_mention() {
|
fn test_actor_address_parse_mention() {
|
||||||
let value = "@user_1@example.com";
|
let value = "@user_1@example.com";
|
||||||
let result = value.parse::<ActorAddress>();
|
let result = value.parse::<ActorAddress>();
|
||||||
assert_eq!(result.is_err(), true);
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -164,6 +164,6 @@ mod tests {
|
||||||
|
|
||||||
let short_mention = "@user";
|
let short_mention = "@user";
|
||||||
let result = ActorAddress::from_mention(short_mention);
|
let result = ActorAddress::from_mention(short_mention);
|
||||||
assert_eq!(result.is_err(), true);
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,8 @@ use crate::activitypub::{
|
||||||
identifiers::{local_actor_id, local_instance_actor_id, parse_local_actor_id},
|
identifiers::{local_actor_id, local_instance_actor_id, parse_local_actor_id},
|
||||||
};
|
};
|
||||||
use crate::errors::{HttpError, ValidationError};
|
use crate::errors::{HttpError, ValidationError};
|
||||||
|
use crate::media::MediaStorage;
|
||||||
|
use crate::tmdb::lookup_and_create_movie_user;
|
||||||
|
|
||||||
use super::types::{
|
use super::types::{
|
||||||
ActorAddress, JsonResourceDescriptor, Link, WebfingerQueryParams, JRD_CONTENT_TYPE,
|
ActorAddress, JsonResourceDescriptor, Link, WebfingerQueryParams, JRD_CONTENT_TYPE,
|
||||||
|
@ -82,8 +84,34 @@ pub async fn webfinger_view(
|
||||||
db_pool: web::Data<DbPool>,
|
db_pool: web::Data<DbPool>,
|
||||||
query_params: web::Query<WebfingerQueryParams>,
|
query_params: web::Query<WebfingerQueryParams>,
|
||||||
) -> Result<HttpResponse, HttpError> {
|
) -> Result<HttpResponse, HttpError> {
|
||||||
let db_client = &**get_database_client(&db_pool).await?;
|
let db_client = &mut **get_database_client(&db_pool).await?;
|
||||||
let jrd = get_jrd(db_client, config.instance(), &query_params.resource).await?;
|
let jrd = match get_jrd(db_client, config.instance(), &query_params.resource).await {
|
||||||
|
Ok(jrd) => jrd,
|
||||||
|
Err(_) => {
|
||||||
|
// Lookup the movie in TMDB and create a local user. By now we know that the local
|
||||||
|
// user for this movie does not exist.
|
||||||
|
let config: &Config = &config;
|
||||||
|
if let Some(api_key) = &config.tmdb_api_key {
|
||||||
|
let movie_account = parse_acct_uri(&query_params.resource)?;
|
||||||
|
let instance = config.instance();
|
||||||
|
let storage = MediaStorage::from(config);
|
||||||
|
lookup_and_create_movie_user(
|
||||||
|
&instance,
|
||||||
|
db_client,
|
||||||
|
api_key,
|
||||||
|
&storage.media_dir,
|
||||||
|
&movie_account.username,
|
||||||
|
config.movie_user_password.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
log::error!("Failed to create movie user: {}", err);
|
||||||
|
HttpError::InternalError
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
get_jrd(db_client, config.instance(), &query_params.resource).await?
|
||||||
|
}
|
||||||
|
};
|
||||||
let response = HttpResponse::Ok().content_type(JRD_CONTENT_TYPE).json(jrd);
|
let response = HttpResponse::Ok().content_type(JRD_CONTENT_TYPE).json(jrd);
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue