Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add expired/actor_target check to has_muted_word #211

Merged
merged 2 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions bsky-sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ anyhow.workspace = true
async-trait.workspace = true
atrium-api.workspace = true
atrium-xrpc-client = { workspace = true, optional = true }
chrono.workspace = true
psl = { version = "2.1.42", optional = true }
regex.workspace = true
serde = { workspace = true, features = ["derive"] }
Expand Down
37 changes: 31 additions & 6 deletions bsky-sdk/src/moderation/mutewords.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Muteword checking logic.
use atrium_api::app::bsky::{actor::defs::MutedWord, richtext::facet::MainFeaturesItem};
use atrium_api::types::Union;
use atrium_api::app::bsky::actor::defs::{MutedWord, ProfileViewBasic};
use atrium_api::app::bsky::richtext::facet;
use atrium_api::types::{string::Language, Union};
use regex::Regex;
use std::sync::OnceLock;

Expand All @@ -27,9 +28,10 @@ const LANGUAGE_EXCEPTIONS: [&str; 5] = [
pub fn has_muted_word(
muted_words: &[MutedWord],
text: &str,
facets: &Option<Vec<atrium_api::app::bsky::richtext::facet::Main>>,
outline_tags: &Option<Vec<String>>,
langs: &Option<Vec<atrium_api::types::string::Language>>,
facets: Option<&Vec<facet::Main>>,
outline_tags: Option<&Vec<String>>,
langs: Option<&Vec<Language>>,
actor: Option<&ProfileViewBasic>,
) -> bool {
let exception = langs
.as_ref()
Expand All @@ -47,7 +49,7 @@ pub fn has_muted_word(
.iter()
.flat_map(|facet| {
facet.features.iter().filter_map(|feature| {
if let Union::Refs(MainFeaturesItem::Tag(tag)) = feature {
if let Union::Refs(facet::MainFeaturesItem::Tag(tag)) = feature {
Some(&tag.tag)
} else {
None
Expand All @@ -61,6 +63,29 @@ pub fn has_muted_word(
for mute in muted_words {
let muted_word = mute.value.to_lowercase();
let post_text = text.to_lowercase();

// check if expired
if let Some(expires_at) = &mute.expires_at {
if expires_at.as_ref() < &chrono::Utc::now().fixed_offset() {
continue;
}
}
// check if actor target
if let Some(actor_target) = &mute.actor_target {
if actor_target == "exclude-following"
&& actor
.and_then(|actor| {
actor
.viewer
.as_ref()
.and_then(|viewer| viewer.following.as_ref())
})
.is_some()
{
continue;
}
}

// `content` applies to tags as well
if tags.contains(&muted_word) {
return true;
Expand Down
203 changes: 190 additions & 13 deletions bsky-sdk/src/moderation/subjects/post.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ use super::super::types::{LabelTarget, SubjectPost};
use super::super::Moderator;
use atrium_api::app::bsky::actor::defs::MutedWord;
use atrium_api::app::bsky::embed::record::{ViewBlocked, ViewRecord, ViewRecordRefs};
use atrium_api::app::bsky::embed::record_with_media::{MainMediaRefs, ViewMediaRefs};
use atrium_api::app::bsky::feed::defs::PostViewEmbedRefs;
use atrium_api::app::bsky::feed::post::{self, RecordEmbedRefs};
use atrium_api::types::{TryFromUnknown, Union};

impl Moderator {
Expand Down Expand Up @@ -137,20 +139,195 @@ fn check_muted_words(subject: &SubjectPost, muted_words: &[MutedWord]) -> bool {
if muted_words.is_empty() {
return false;
}
let Ok(post) =
atrium_api::app::bsky::feed::post::Record::try_from_unknown(subject.data.record.clone())
else {
return false;
};
if has_muted_word(
muted_words,
&post.text,
&post.facets,
&post.tags,
&post.langs,
) {
return true;

let post_author = &subject.author;

if let Ok(post) =
atrium_api::app::bsky::feed::post::Record::try_from_unknown(subject.record.clone())
{
// post text
if has_muted_word(
muted_words,
&post.text,
post.facets.as_ref(),
post.tags.as_ref(),
post.langs.as_ref(),
Some(post_author),
) {
return true;
}

if let Some(Union::Refs(RecordEmbedRefs::AppBskyEmbedImagesMain(images))) = &post.embed {
// post images
for image in &images.images {
if has_muted_word(
muted_words,
&image.alt,
None,
None,
post.langs.as_ref(),
Some(post_author),
) {
return true;
}
}
}
}

match &subject.embed {
// quote post
Some(Union::Refs(PostViewEmbedRefs::AppBskyEmbedRecordView(view))) => {
if let Union::Refs(ViewRecordRefs::ViewRecord(view_record)) = &view.record {
if let Ok(record) = post::Record::try_from_unknown(view_record.value.clone()) {
let embedded_post = record;
let embed_author = &view_record.author;
// quoted post text
if has_muted_word(
muted_words,
&embedded_post.text,
embedded_post.facets.as_ref(),
embedded_post.tags.as_ref(),
embedded_post.langs.as_ref(),
Some(embed_author),
) {
return true;
}
match &embedded_post.embed {
// quoted post's images
Some(Union::Refs(RecordEmbedRefs::AppBskyEmbedImagesMain(main))) => {
for image in &main.images {
if has_muted_word(
muted_words,
&image.alt,
None,
None,
embedded_post.langs.as_ref(),
Some(embed_author),
) {
return true;
}
}
}
// quoted post's link card
Some(Union::Refs(RecordEmbedRefs::AppBskyEmbedExternalMain(main))) => {
let external = &main.external;
if has_muted_word(
muted_words,
&format!("{} {}", external.title, external.description),
None,
None,
None,
Some(embed_author),
) {
return true;
}
}
Some(Union::Refs(RecordEmbedRefs::AppBskyEmbedRecordWithMediaMain(
main,
))) => match &main.media {
Union::Refs(MainMediaRefs::AppBskyEmbedExternalMain(main)) => {
let external = &main.external;
if has_muted_word(
muted_words,
&format!("{} {}", external.title, external.description),
None,
None,
None,
Some(embed_author),
) {
return true;
}
}
Union::Refs(MainMediaRefs::AppBskyEmbedImagesMain(main)) => {
for image in &main.images {
if has_muted_word(
muted_words,
&image.alt,
None,
None,
embedded_post.langs.as_ref(),
Some(embed_author),
) {
return true;
}
}
}
_ => {}
},
_ => {}
}
}
}
}
// link card
Some(Union::Refs(PostViewEmbedRefs::AppBskyEmbedExternalView(view))) => {
let external = &view.external;
if has_muted_word(
muted_words,
&format!("{} {}", external.title, external.description),
None,
None,
None,
Some(post_author),
) {
return true;
}
}
// quote post with media
Some(Union::Refs(PostViewEmbedRefs::AppBskyEmbedRecordWithMediaView(view))) => {
if let Union::Refs(ViewRecordRefs::ViewRecord(view_record)) = &view.record.record {
let embed_author = &view_record.author;
// quoted post text
if let Ok(record) = post::Record::try_from_unknown(view_record.value.clone()) {
let post = record;
if has_muted_word(
muted_words,
&post.text,
post.facets.as_ref(),
post.tags.as_ref(),
post.langs.as_ref(),
Some(embed_author),
) {
return true;
}
}
// quoted post media
match &view.media {
Union::Refs(ViewMediaRefs::AppBskyEmbedExternalView(view)) => {
let external = &view.external;
if has_muted_word(
muted_words,
&format!("{} {}", external.title, external.description),
None,
None,
None,
Some(embed_author),
) {
return true;
}
}
Union::Refs(ViewMediaRefs::AppBskyEmbedImagesView(view)) => {
let langs = post::Record::try_from_unknown(view_record.value.clone())
.ok()
.and_then(|record| record.data.langs);
for image in &view.images {
if has_muted_word(
muted_words,
&image.alt,
None,
None,
langs.as_ref(),
Some(embed_author),
) {
return true;
}
}
}
_ => {}
}
}
}
_ => {}
}
false
}
Loading
Loading