🐸 Frogue(Fed)

~ololduck/Frogue

Raw

// SPDX-FileCopyrightText: 2026 Ololduck <ololduck@vit.am> and contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later

//! Moderation reports handling.

use actix_web::HttpResponse;
use sea_orm::ActiveValue::NotSet;
use sea_orm::{ActiveModelTrait, DatabaseConnection, Set};
use tracing::{debug, info, instrument, warn};
use uuid::Uuid;

use crate::activitypub::actor::get_by_url;
use crate::activitypub::types::FlagActivity;
use crate::entities::{ap_actors, reports};

/// Handle an incoming Flag activity (moderation report).
///
/// Creates a moderation report when a remote actor flags content. The report
/// is stored in the database and can be reviewed by instance administrators.
///
/// # Arguments
///
/// * `db` - Database connection
/// * `public_url` - The public URL of this instance
/// * `flag` - The Flag activity containing the report
/// * `signer` - The actor who signed the activity (must match flag.actor)
///
/// # Returns
///
/// Returns 202 Accepted on success (even for duplicates).
#[instrument(name = "inbox.handle_flag", skip(db, flag, signer), fields(flag_id = %flag.id, actor = %signer.user_id.unwrap_or(0)))]
pub async fn handle_flag(
    db: &DatabaseConnection,
    public_url: &str,
    flag: &FlagActivity,
    signer: &ap_actors::Model,
) -> HttpResponse {
    // Extract reported URLs
    let reported_urls = flag.object.urls();

    if reported_urls.is_empty() {
        warn!("Flag activity contains no reported URLs");
        return HttpResponse::Accepted().finish();
    }

    // The first URL is the reported actor (Mastodon semantics)
    let reported_actor_url = reported_urls[0];

    debug!(
        reported_actor_url = %reported_actor_url,
        num_objects = %reported_urls.len(),
        "Processing Flag activity"
    );

    // Resolve the reported actor to a local user if possible
    let target_local_user_id = match get_by_url(db, reported_actor_url, public_url).await {
        Ok(Some(actor)) => {
            info!(
                target_user_id = %actor.user_id.unwrap_or(0),
                "Resolved reported actor to local user"
            );
            actor.user_id
        }
        Ok(None) => {
            debug!(
                reported_actor_url = %reported_actor_url,
                "Reported actor not found locally"
            );
            None
        }
        Err(e) => {
            warn!(
                error = %e,
                reported_actor_url = %reported_actor_url,
                "Failed to resolve reported actor"
            );
            None
        }
    };

    // Serialize reported URLs to JSON array
    let reported_urls_json = match serde_json::to_string(&reported_urls) {
        Ok(json) => json,
        Err(e) => {
            warn!(error = %e, "Failed to serialize reported URLs");
            return HttpResponse::Accepted().finish();
        }
    };

    // Determine if this is a forwarded report (sent to instance actor)
    // Instance actor has user_id=None and repo_id=None
    let forwarded = if signer.user_id.is_none() && signer.repo_id.is_none() {
        1
    } else {
        0
    };

    // Create the report
    let report = reports::ActiveModel {
        id: NotSet,
        uuid: Set(Uuid::new_v4().to_string()),
        created_at: Set(chrono::Utc::now().into()),
        reporter_local_user_id: Set(None),
        reporter_remote_actor_url: Set(Some(flag.actor.clone())),
        target_local_user_id: Set(target_local_user_id),
        target_remote_actor_id: Set(None),
        object_urls: Set(reported_urls_json),
        comment: Set(flag.content.clone()),
        state: Set("open".to_string()),
        forwarded: Set(forwarded),
        inbound: Set(1),
        activity_url: Set(Some(flag.id.clone())),
    };

    match report.insert(db).await {
        Ok(_) => {
            info!(
                flag_id = %flag.id,
                reporter = %flag.actor,
                target_user_id = ?target_local_user_id,
                forwarded = %forwarded,
                "Created moderation report"
            );
            HttpResponse::Accepted().finish()
        }
        Err(e) => {
            // Check if this is a duplicate (UNIQUE constraint on activity_url)
            if e.to_string().contains("UNIQUE constraint failed") {
                debug!(
                    flag_id = %flag.id,
                    "Duplicate Flag activity, ignoring"
                );
                return HttpResponse::Accepted().finish();
            }

            warn!(
                error = %e,
                flag_id = %flag.id,
                "Failed to insert moderation report"
            );
            HttpResponse::Accepted().finish()
        }
    }
}