jacquard 0.12.0-beta.2

Simple and powerful AT Protocol client library for Rust
Documentation
use super::{LabelerDefs, ModerationDecision, ModerationPrefs, moderate};
use jacquard_common::bos::BosStr;
use jacquard_common::types::string::Did;

/// Trait for composite types that contain multiple labeled items
///
/// Types like `FeedViewPost` contain several pieces that each have their own labels
/// (post, author, reply chain, embeds, etc.). This trait allows them to return
/// moderation decisions for all their parts, tagged with identifiers so consumers
/// can handle each part appropriately.
///
/// # Example
///
/// ```ignore
/// # use jacquard::moderation::*;
/// # use jacquard_api::app_bsky::feed::FeedViewPost;
/// # fn example(feed_post: &FeedViewPost, prefs: &ModerationPrefs, defs: &LabelerDefs) {
/// for (tag, decision) in feed_post.moderate_all(prefs, defs, &[]) {
///     match tag {
///         "post" if decision.filter => println!("Hide post content"),
///         "author" if decision.filter => println!("Hide author info"),
///         _ => {}
///     }
/// }
/// # }
/// ```
pub trait Moderateable<S: BosStr> {
    /// Apply moderation to all labeled parts of this item
    ///
    /// Returns a vector of (tag, decision) tuples where the tag identifies
    /// which part of the composite item the decision applies to.
    fn moderate_all(
        &self,
        prefs: &ModerationPrefs,
        defs: &LabelerDefs,
        accepted_labelers: &[Did],
    ) -> Vec<(&'static str, ModerationDecision)>;
}

/// Extension trait for applying moderation to iterators
///
/// Provides convenience methods for filtering and mapping moderation decisions
/// over collections.
pub trait ModeratableIterExt<'a, S: BosStr, T: Moderateable<S> + 'a>:
    Iterator<Item = &'a T> + Sized
{
    /// Map each item to a tuple of (item, decision)
    fn with_moderation(
        self,
        prefs: &'a ModerationPrefs,
        defs: &'a LabelerDefs,
        accepted_labelers: &'a [Did],
    ) -> impl Iterator<Item = (&'a T, Vec<(&'static str, ModerationDecision)>)> {
        self.map(move |item| {
            let scoped_decisions = item.moderate_all(prefs, defs, accepted_labelers);
            (item, scoped_decisions)
        })
    }

    /// Filter out items that should be hidden
    fn filter_moderated(
        self,
        prefs: &'a ModerationPrefs,
        defs: &'a LabelerDefs,
        accepted_labelers: &'a [Did],
    ) -> impl Iterator<Item = &'a T> {
        self.filter(move |item| {
            let scoped_decisions = item.moderate_all(prefs, defs, accepted_labelers);
            !scoped_decisions.iter().any(|(_, decision)| decision.filter)
        })
    }
}

impl<'a, S: BosStr, T: Moderateable<S> + 'a, I: Iterator<Item = &'a T>> ModeratableIterExt<'a, S, T>
    for I
{
}

// Implementations for common Bluesky types
#[cfg(feature = "api_bluesky")]
mod bluesky_impls {
    use super::*;
    use jacquard_api::app_bsky::feed::{FeedViewPost, ReplyRefParent, ReplyRefRoot};
    use jacquard_common::bos::DefaultStr;

    impl Moderateable<DefaultStr> for FeedViewPost {
        fn moderate_all(
            &self,
            prefs: &ModerationPrefs,
            defs: &LabelerDefs,
            accepted_labelers: &[Did],
        ) -> Vec<(&'static str, ModerationDecision)> {
            let mut decisions = vec![
                ("post", moderate(&self.post, prefs, defs, accepted_labelers)),
                (
                    "author",
                    moderate(&self.post.author, prefs, defs, accepted_labelers),
                ),
            ];

            // Add reply chain decisions if present
            if let Some(reply) = &self.reply {
                // Parent post and author
                if let ReplyRefParent::PostView(parent) = &reply.parent {
                    decisions.push((
                        "reply_parent",
                        moderate(&**parent, prefs, defs, accepted_labelers),
                    ));
                    decisions.push((
                        "reply_parent_author",
                        moderate(&parent.author, prefs, defs, accepted_labelers),
                    ));
                }

                // Root post and author
                if let ReplyRefRoot::PostView(root) = &reply.root {
                    decisions.push((
                        "reply_root",
                        moderate(&**root, prefs, defs, accepted_labelers),
                    ));
                    decisions.push((
                        "reply_root_author",
                        moderate(&root.author, prefs, defs, accepted_labelers),
                    ));
                }

                // Grandparent author
                if let Some(grandparent_author) = &reply.grandparent_author {
                    decisions.push((
                        "reply_grandparent_author",
                        moderate(grandparent_author, prefs, defs, accepted_labelers),
                    ));
                }
            }

            // TODO: handle embeds (quote posts, external links with metadata, etc.)
            // if let Some(embed) = &self.post.embed {
            //     match embed {
            //         PostViewEmbedRecord(record) => { ... }
            //         ...
            //     }
            // }

            decisions
        }
    }
}