diff --git a/Cargo.toml b/Cargo.toml index 4a5c1cd..096807f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ description = "Federated micro-blogging platform and content subscription servic license = "AGPL-3.0" edition = "2021" -rust-version = "1.57" +rust-version = "1.68" publish = false default-run = "mitra" diff --git a/mitra-cli/Cargo.toml b/mitra-cli/Cargo.toml index 87484f3..ff148b0 100644 --- a/mitra-cli/Cargo.toml +++ b/mitra-cli/Cargo.toml @@ -3,7 +3,7 @@ name = "mitra-cli" version = "1.22.0" license = "AGPL-3.0" edition = "2021" -rust-version = "1.57" +rust-version = "1.68" [[bin]] name = "mitractl" diff --git a/mitra-config/Cargo.toml b/mitra-config/Cargo.toml index 04fd200..9b23ecc 100644 --- a/mitra-config/Cargo.toml +++ b/mitra-config/Cargo.toml @@ -3,7 +3,7 @@ name = "mitra-config" version = "1.22.0" license = "AGPL-3.0" edition = "2021" -rust-version = "1.57" +rust-version = "1.68" [dependencies] mitra-utils = { path = "../mitra-utils" } diff --git a/mitra-config/src/config.rs b/mitra-config/src/config.rs index 46bf846..bd9fc86 100644 --- a/mitra-config/src/config.rs +++ b/mitra-config/src/config.rs @@ -53,6 +53,11 @@ pub struct Config { pub instance_short_description: String, pub instance_description: String, + #[serde(default)] + pub tmdb_api_key: Option, + #[serde(default)] + pub movie_user_password: Option, + #[serde(skip)] pub(super) instance_rsa_key: Option, @@ -99,8 +104,8 @@ impl Config { onion_proxy_url: self.federation.onion_proxy_url.clone(), i2p_proxy_url: self.federation.i2p_proxy_url.clone(), // Private instance doesn't send activities and sign requests - is_private: !self.federation.enabled - || matches!(self.environment, Environment::Development), + is_private: !self.federation.enabled, + // || matches!(self.environment, Environment::Development), fetcher_timeout: self.federation.fetcher_timeout, deliverer_timeout: self.federation.deliverer_timeout, } diff --git a/mitra-models/Cargo.toml b/mitra-models/Cargo.toml index 84512dc..e5bbba3 100644 --- a/mitra-models/Cargo.toml +++ b/mitra-models/Cargo.toml @@ -3,7 +3,7 @@ name = "mitra-models" version = "1.22.0" license = "AGPL-3.0" edition = "2021" -rust-version = "1.57" +rust-version = "1.68" [dependencies] mitra-utils = { path = "../mitra-utils" } diff --git a/mitra-models/src/notifications/queries.rs b/mitra-models/src/notifications/queries.rs index fb51bad..09cbb5f 100644 --- a/mitra-models/src/notifications/queries.rs +++ b/mitra-models/src/notifications/queries.rs @@ -32,6 +32,22 @@ async fn create_notification( 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( db_client: &impl DatabaseClient, sender_id: &Uuid, @@ -151,7 +167,7 @@ pub async fn get_notifications( let statement = format!( " SELECT - notification, sender, post, post_author, + notification, sender, post, post_author, recipient, {related_attachments}, {related_mentions}, {related_tags}, @@ -164,6 +180,8 @@ pub async fn get_notifications( 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 recipient_id = $1 AND ($2::integer IS NULL OR notification.id < $2) @@ -202,3 +220,52 @@ pub async fn get_notifications( .await?; Ok(notifications) } + +pub async fn get_mention_notifications( + db_client: &impl DatabaseClient, + limit: u16, +) -> Result, 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 = rows + .iter() + .map(Notification::try_from) + .collect::>()?; + Ok(notifications) +} diff --git a/mitra-models/src/notifications/types.rs b/mitra-models/src/notifications/types.rs index b740b18..dff4764 100644 --- a/mitra-models/src/notifications/types.rs +++ b/mitra-models/src/notifications/types.rs @@ -82,6 +82,7 @@ struct DbNotification { pub struct Notification { pub id: i32, pub sender: DbActorProfile, + pub recipient: DbActorProfile, pub post: Option, pub event_type: EventType, pub created_at: DateTime, @@ -93,6 +94,7 @@ impl TryFrom<&Row> for Notification { fn try_from(row: &Row) -> Result { let db_notification: DbNotification = row.try_get("notification")?; let db_sender: DbActorProfile = row.try_get("sender")?; + let db_recipient: DbActorProfile = row.try_get("recipient")?; let maybe_db_post: Option = row.try_get("post")?; let maybe_post = match maybe_db_post { Some(db_post) => { @@ -118,6 +120,7 @@ impl TryFrom<&Row> for Notification { let notification = Self { id: db_notification.id, sender: db_sender, + recipient: db_recipient, post: maybe_post, event_type: db_notification.event_type, created_at: db_notification.created_at, diff --git a/mitra-models/src/posts/types.rs b/mitra-models/src/posts/types.rs index 2dcc996..ad95913 100644 --- a/mitra-models/src/posts/types.rs +++ b/mitra-models/src/posts/types.rs @@ -262,7 +262,7 @@ impl PostCreateData { pub fn repost(repost_of_id: Uuid, object_id: Option) -> Self { Self { repost_of_id: Some(repost_of_id), - object_id: object_id, + object_id, created_at: Utc::now(), ..Default::default() } diff --git a/mitra-models/src/users/queries.rs b/mitra-models/src/users/queries.rs index 369c688..3b997df 100644 --- a/mitra-models/src/users/queries.rs +++ b/mitra-models/src/users/queries.rs @@ -249,7 +249,7 @@ pub async fn get_user_by_name( " SELECT user_account, actor_profile FROM user_account JOIN actor_profile USING (id) - WHERE actor_profile.username = $1 + WHERE lower(actor_profile.username) = lower($1) ", &[&username], ) diff --git a/mitra-utils/Cargo.toml b/mitra-utils/Cargo.toml index 6e9fa52..924f6ec 100644 --- a/mitra-utils/Cargo.toml +++ b/mitra-utils/Cargo.toml @@ -3,7 +3,7 @@ name = "mitra-utils" version = "1.22.0" license = "AGPL-3.0" edition = "2021" -rust-version = "1.57" +rust-version = "1.68" [dependencies] # Used for HTML sanitization diff --git a/mitra-utils/src/urls.rs b/mitra-utils/src/urls.rs index 356ef77..dabba66 100644 --- a/mitra-utils/src/urls.rs +++ b/mitra-utils/src/urls.rs @@ -11,7 +11,7 @@ pub fn get_hostname(url: &str) -> Result { } pub fn guess_protocol(hostname: &str) -> &'static str { - if hostname == "localhost" || hostname.ends_with(".example.com") { + if hostname == "localhost" { return "http"; }; let maybe_ipv4_address = hostname.parse::(); diff --git a/src/activitypub/actors/types.rs b/src/activitypub/actors/types.rs index 861a5d8..e5697d2 100644 --- a/src/activitypub/actors/types.rs +++ b/src/activitypub/actors/types.rs @@ -109,6 +109,8 @@ pub struct Actor { pub inbox: String, pub outbox: String, + pub bot: bool, + #[serde(skip_serializing_if = "Option::is_none")] pub followers: Option, @@ -325,6 +327,7 @@ pub fn get_local_actor(user: &User, instance_url: &str) -> Result Result { preferred_username: instance.hostname(), inbox: actor_inbox, outbox: actor_outbox, + bot: true, followers: None, following: None, subscribers: None, diff --git a/src/activitypub/fetcher/helpers.rs b/src/activitypub/fetcher/helpers.rs index 8cbcaf5..541e1f3 100644 --- a/src/activitypub/fetcher/helpers.rs +++ b/src/activitypub/fetcher/helpers.rs @@ -153,6 +153,8 @@ pub async fn import_post( db_client: &mut impl DatabaseClient, instance: &Instance, storage: &MediaStorage, + tmdb_api_key: Option, + default_movie_user_password: Option, object_id: String, object_received: Option, ) -> Result { @@ -245,7 +247,16 @@ pub async fn import_post( // starting with the root objects.reverse(); 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); } diff --git a/src/activitypub/handlers/announce.rs b/src/activitypub/handlers/announce.rs index c544c58..0405f27 100644 --- a/src/activitypub/handlers/announce.rs +++ b/src/activitypub/handlers/announce.rs @@ -54,7 +54,18 @@ pub async fn handle_announce( Ok(post_id) => post_id, Err(_) => { // 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 } }; diff --git a/src/activitypub/handlers/create.rs b/src/activitypub/handlers/create.rs index 42ad76a..ef3f75b 100644 --- a/src/activitypub/handlers/create.rs +++ b/src/activitypub/handlers/create.rs @@ -36,6 +36,7 @@ use crate::activitypub::{ }; use crate::errors::ValidationError; use crate::media::MediaStorage; +use crate::tmdb::lookup_and_create_movie_user; use crate::validators::{ emojis::{validate_emoji_name, EMOJI_MEDIA_TYPES}, posts::{ @@ -306,6 +307,8 @@ pub async fn get_object_tags( db_client: &mut impl DatabaseClient, instance: &Instance, storage: &MediaStorage, + api_key: Option, + default_movie_user_password: Option, object: &Object, redirects: &HashMap, ) -> Result<(Vec, Vec, Vec, Vec), HandlerError> { @@ -346,7 +349,33 @@ pub async fn get_object_tags( // Try to find profile by actor ID. if let Some(href) = tag.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) { mentions.push(user.id); }; @@ -503,6 +532,8 @@ pub async fn handle_note( db_client: &mut impl DatabaseClient, instance: &Instance, storage: &MediaStorage, + tmdb_api_key: Option, + default_movie_user_password: Option, object: Object, redirects: &HashMap, ) -> Result { @@ -543,8 +574,16 @@ pub async fn handle_note( return Err(ValidationError("post is empty").into()); }; - let (mentions, hashtags, links, emojis) = - get_object_tags(db_client, instance, storage, &object, redirects).await?; + let (mentions, hashtags, links, emojis) = get_object_tags( + db_client, + instance, + storage, + tmdb_api_key, + default_movie_user_password, + &object, + redirects, + ) + .await?; let in_reply_to_id = match object.in_reply_to { Some(ref object_id) => { @@ -632,10 +671,15 @@ pub async fn handle_create( // Most likely it's a forwarded reply. None }; + + let tmdb_api_key = config.tmdb_api_key.clone(); + let default_movie_user_password = config.movie_user_password.clone(); import_post( db_client, &config.instance(), &MediaStorage::from(config), + tmdb_api_key, + default_movie_user_password, object_id, object_received, ) diff --git a/src/activitypub/handlers/update.rs b/src/activitypub/handlers/update.rs index 97773e5..f23aec4 100644 --- a/src/activitypub/handlers/update.rs +++ b/src/activitypub/handlers/update.rs @@ -67,8 +67,19 @@ async fn handle_update_note( if content.is_empty() && attachments.is_empty() { 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 post_data = PostUpdateData { content, diff --git a/src/activitypub/identifiers.rs b/src/activitypub/identifiers.rs index 77dcdb0..817a983 100644 --- a/src/activitypub/identifiers.rs +++ b/src/activitypub/identifiers.rs @@ -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 { let url_regexp_str = format!( - "^{}/users/(?P[0-9a-z_]+)$", + "^{}/users/(?P[0-9a-zA-Z_]+)$", instance_url.replace('.', r"\."), ); let url_regexp = Regex::new(&url_regexp_str).map_err(|_| ValidationError("error"))?; diff --git a/src/job_queue/periodic_tasks.rs b/src/job_queue/periodic_tasks.rs index f514845..dca9848 100644 --- a/src/job_queue/periodic_tasks.rs +++ b/src/job_queue/periodic_tasks.rs @@ -7,7 +7,13 @@ use mitra_models::{ posts::queries::{delete_post, find_extraneous_posts}, 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 crate::activitypub::builders::announce::prepare_announce; use crate::activitypub::queues::{ 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(()) } + +// 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?) +} diff --git a/src/job_queue/scheduler.rs b/src/job_queue/scheduler.rs index 45cf300..14fcef4 100644 --- a/src/job_queue/scheduler.rs +++ b/src/job_queue/scheduler.rs @@ -16,6 +16,7 @@ enum PeriodicTask { DeleteExtraneousPosts, DeleteEmptyProfiles, PruneRemoteEmojis, + HandleMoviesMentions, } impl PeriodicTask { @@ -28,6 +29,7 @@ impl PeriodicTask { Self::DeleteExtraneousPosts => 3600, Self::DeleteEmptyProfiles => 3600, Self::PruneRemoteEmojis => 3600, + Self::HandleMoviesMentions => 5, } } @@ -49,6 +51,7 @@ pub fn run(config: Config, db_pool: DbPool) -> () { (PeriodicTask::IncomingActivityQueueExecutor, None), (PeriodicTask::OutgoingActivityQueueExecutor, None), (PeriodicTask::PruneRemoteEmojis, None), + (PeriodicTask::HandleMoviesMentions, None), ]); if config.retention.extraneous_posts.is_some() { 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::SubscriptionExpirationMonitor => Ok(()), + PeriodicTask::HandleMoviesMentions => handle_movies_mentions(&config, &db_pool).await, }; task_result.unwrap_or_else(|err| { log::error!("{:?}: {}", task, err); diff --git a/src/lib.rs b/src/lib.rs index 5b19c38..abe7baa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,7 @@ pub mod logger; pub mod mastodon_api; pub mod media; pub mod nodeinfo; +mod tmdb; pub mod validators; pub mod web_client; pub mod webfinger; diff --git a/src/mastodon_api/search/helpers.rs b/src/mastodon_api/search/helpers.rs index 8439d6c..6e47fd4 100644 --- a/src/mastodon_api/search/helpers.rs +++ b/src/mastodon_api/search/helpers.rs @@ -155,7 +155,19 @@ async fn find_post_by_url( } Err(_) => { 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), Err(err) => { log::warn!("{}", err); diff --git a/src/mastodon_api/statuses/microsyntax/mentions.rs b/src/mastodon_api/statuses/microsyntax/mentions.rs index bd9d4c1..62b20c3 100644 --- a/src/mastodon_api/statuses/microsyntax/mentions.rs +++ b/src/mastodon_api/statuses/microsyntax/mentions.rs @@ -15,7 +15,7 @@ use crate::webfinger::types::ActorAddress; // See also: ACTOR_ADDRESS_RE in webfinger::types const MENTION_SEARCH_RE: &str = r"(?m)(?P^|\s|>|[\(])@(?P[^\s<]+)"; const MENTION_SEARCH_SECONDARY_RE: &str = - r"^(?P[\w\.-]+)(@(?P[\w\.-]+\w))?(?P[\.,:?!\)]?)$"; + r"^(?P[\w\.-_]+)(@(?P[\w\.-]+\w))?(?P[\.,:?!\)]?)$"; /// Finds everything that looks like a mention fn find_mentions(instance_hostname: &str, text: &str) -> Vec { diff --git a/src/mastodon_api/subscriptions/views.rs b/src/mastodon_api/subscriptions/views.rs index 348e456..0c7c267 100644 --- a/src/mastodon_api/subscriptions/views.rs +++ b/src/mastodon_api/subscriptions/views.rs @@ -28,11 +28,8 @@ pub async fn authorize_subscription( ) -> Result { let db_client = &**get_database_client(&db_pool).await?; let _current_user = get_current_user(db_client, auth.token()).await?; - - // The user must have a public ethereum address, - // because subscribers should be able - // to verify that payments are actually sent to the recipient. - return Err(MastodonError::PermissionError); + // We don't have subscriptions + Err(MastodonError::PermissionError) } #[get("/options")] diff --git a/src/validators/users.rs b/src/validators/users.rs index 74bef54..0fb1d54 100644 --- a/src/validators/users.rs +++ b/src/validators/users.rs @@ -7,7 +7,7 @@ use super::profiles::validate_username; pub fn validate_local_username(username: &str) -> Result<(), ValidationError> { validate_username(username)?; // 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) { return Err(ValidationError("invalid username")); }; diff --git a/src/webfinger/types.rs b/src/webfinger/types.rs index ff9f854..267b4db 100644 --- a/src/webfinger/types.rs +++ b/src/webfinger/types.rs @@ -135,9 +135,9 @@ mod tests { #[test] 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(); - 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.to_string(), value); } @@ -146,7 +146,7 @@ mod tests { fn test_actor_address_parse_mention() { let value = "@user_1@example.com"; let result = value.parse::(); - assert_eq!(result.is_err(), true); + assert!(result.is_err()); } #[test] @@ -164,6 +164,6 @@ mod tests { let short_mention = "@user"; let result = ActorAddress::from_mention(short_mention); - assert_eq!(result.is_err(), true); + assert!(result.is_err()); } } diff --git a/src/webfinger/views.rs b/src/webfinger/views.rs index 4ce24dd..e51c19d 100644 --- a/src/webfinger/views.rs +++ b/src/webfinger/views.rs @@ -11,6 +11,8 @@ use crate::activitypub::{ identifiers::{local_actor_id, local_instance_actor_id, parse_local_actor_id}, }; use crate::errors::{HttpError, ValidationError}; +use crate::media::MediaStorage; +use crate::tmdb::lookup_and_create_movie_user; use super::types::{ ActorAddress, JsonResourceDescriptor, Link, WebfingerQueryParams, JRD_CONTENT_TYPE, @@ -82,8 +84,34 @@ pub async fn webfinger_view( db_pool: web::Data, query_params: web::Query, ) -> Result { - let db_client = &**get_database_client(&db_pool).await?; - let jrd = get_jrd(db_client, config.instance(), &query_params.resource).await?; + let db_client = &mut **get_database_client(&db_pool).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); Ok(response) }