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

respect content warnings #76

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions src/post.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ fn send_single_post_to_mastodon(mastodon: &Mastodon, toot: &NewStatus) -> Result

let mut status_builder = StatusBuilder::new();
status_builder.status(&toot.text);
if let Some(spoiler) = toot.content_warning.as_ref() {
status_builder.sensitive(true);
status_builder.spoiler_text(spoiler);
}
status_builder.media_ids(media_ids);
if let Some(parent_id) = toot.in_reply_to_id {
status_builder.in_reply_to(parent_id.to_string());
Expand Down
94 changes: 90 additions & 4 deletions src/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ use anyhow::Result;
use egg_mode::tweet::Tweet;
use egg_mode_text::character_count;
use elefren::entities::status::Status;
use regex::Regex;
use regex::{Regex, RegexBuilder};
use std::collections::HashSet;
use std::fs;
use unicode_segmentation::UnicodeSegmentation;

const TWITTER_CW_REGEX: &'static str = r"^(RT .*: )?CW: (.*?\n)(.*)$";

// Represents new status updates that should be posted to Twitter (tweets) and
// Mastodon (toots).
#[derive(Debug, Clone)]
Expand All @@ -30,6 +32,7 @@ impl StatusUpdates {
pub struct NewStatus {
pub text: String,
pub attachments: Vec<NewMedia>,
pub content_warning: Option<String>,
// A list of further statuses that are new replies to this new status. Used
// to sync threads.
pub replies: Vec<NewStatus>,
Expand Down Expand Up @@ -99,7 +102,13 @@ pub fn determine_posts(

// The tweet is not on Mastodon yet, check if we should post it.
// Fetch the tweet text into a String object
let decoded_tweet = tweet_unshorten_decode(tweet);
let mut decoded_tweet = tweet_unshorten_decode(tweet);

// Check for a content warning
let content_warning = tweet_find_content_warning(&decoded_tweet);

// If present, strip CW from post text. Mastodon has a dedicated field for that
decoded_tweet = tweet_strip_content_warning(&decoded_tweet);

// Check if hashtag filtering is enabled and if the tweet matches.
if let Some(sync_hashtag) = &options.sync_hashtag_twitter {
Expand All @@ -111,6 +120,7 @@ pub fn determine_posts(

updates.toots.push(NewStatus {
text: decoded_tweet,
content_warning,
attachments: tweet_get_attachments(tweet),
replies: Vec::new(),
in_reply_to_id: None,
Expand All @@ -128,7 +138,15 @@ pub fn determine_posts(
// Skip reblogs when sync_reblogs is disabled
continue;
}
let fulltext = mastodon_toot_get_text(toot);
let mut fulltext = mastodon_toot_get_text(toot);
let mut content_warning: Option<String> = None;

// add content_warning if present
if toot.spoiler_text.len() > 0 {
fulltext = add_content_warning_to_post_text(&fulltext, toot.spoiler_text.as_str());
content_warning = Some(toot.spoiler_text.clone());
}

// If this is a reblog/boost then take the URL to the original toot.
let post = match &toot.reblog {
None => tweet_shorten(&fulltext, &toot.url),
Expand Down Expand Up @@ -158,6 +176,7 @@ pub fn determine_posts(

updates.tweets.push(NewStatus {
text: post,
content_warning,
attachments: toot_get_attachments(toot),
replies: Vec::new(),
in_reply_to_id: None,
Expand Down Expand Up @@ -187,7 +206,13 @@ pub fn toot_and_tweet_are_equal(toot: &Status, tweet: &Tweet) -> bool {
}

// Strip markup from Mastodon toot and unify message for comparison.
let toot_text = unify_post_content(mastodon_toot_get_text(toot));
let mut toot_text = unify_post_content(mastodon_toot_get_text(toot));

// if toot has a spoiler add for comparison
if toot.spoiler_text.len() > 0 {
toot_text = add_content_warning_to_post_text(&toot_text, toot.spoiler_text.as_str());
}

// Replace those ugly t.co URLs in the tweet text.
let tweet_text = unify_post_content(tweet_unshorten_decode(tweet));

Expand Down Expand Up @@ -544,6 +569,44 @@ fn truncate_option_string(stringy: Option<String>, max_chars: usize) -> Option<S
}
}

/// adds a post's content warning inline post, as twitter doesn't support any special place for it
/// extracted to dedicated function for consistent generation and central place to change if needed.
pub fn add_content_warning_to_post_text(post_text: &str, content_warning: &str) -> String {
format!("CW: {}\n\n{}", content_warning, post_text)
}

/// searches for a inline-content warning
pub fn tweet_find_content_warning(decoded_tweet: &str) -> Option<String> {
// Check for a content warning
let re = RegexBuilder::new(TWITTER_CW_REGEX)
.dot_matches_new_line(true)
.build()
.unwrap();

match re.captures(decoded_tweet) {
Some(captures) => Some(captures.get(2).unwrap().as_str().trim().to_string()),
None => None,
}
}

/// strips an inline-content warning if present
pub fn tweet_strip_content_warning(decoded_tweet: &str) -> String {
// Check for a content warning
let re = RegexBuilder::new(TWITTER_CW_REGEX)
.dot_matches_new_line(true)
.build()
.unwrap();

match re.captures(decoded_tweet) {
Some(captures) => format!(
"{}{}",
captures.get(1).map_or("", |m| m.as_str()),
captures.get(3).unwrap().as_str(),
),
None => decoded_tweet.to_string(),
}
}

#[cfg(test)]
pub mod tests {

Expand Down Expand Up @@ -1390,6 +1453,29 @@ QT test123: Original text"
assert_eq!(tweet.attachments[0].alt_text, Some("a".repeat(1_000)));
}

#[test]
fn tweet_add_content_warning() {
let fulltext = "blabalblabla";
let spoiler_text = "this is a unittest";
let expected = "CW: ".to_string() + spoiler_text + "\n\n" + fulltext;

assert_eq!(
expected,
add_content_warning_to_post_text(fulltext, spoiler_text)
);
}

#[test]
fn tweet_recognize_content_warning() {
let expected = "Some Unittest dude";
let decoded_tweet = "CW: ".to_string() + expected + "\nsome text";

assert_eq!(
expected,
tweet_find_content_warning(&decoded_tweet).unwrap()
);
}

pub fn get_mastodon_status() -> Status {
read_mastodon_status("src/mastodon_status.json")
}
Expand Down
23 changes: 21 additions & 2 deletions src/thread_replies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use elefren::entities::status::Status;
struct Reply {
pub id: u64,
pub text: String,
pub content_warning: Option<String>,
pub attachments: Vec<NewMedia>,
pub in_reply_to_id: u64,
}
Expand Down Expand Up @@ -43,7 +44,13 @@ pub fn determine_thread_replies(

// The tweet is not on Mastodon yet, check if we should post it.
// Fetch the tweet text into a String object
let decoded_tweet = tweet_unshorten_decode(tweet);
let mut decoded_tweet = tweet_unshorten_decode(tweet);

// Check for a content warning
let content_warning = tweet_find_content_warning(&decoded_tweet);

// If present, strip CW from post text. Mastodon has a dedicated field for that
decoded_tweet = tweet_strip_content_warning(&decoded_tweet);

// Check if hashtag filtering is enabled and if the tweet matches.
if let Some(sync_hashtag) = &options.sync_hashtag_twitter {
Expand All @@ -59,6 +66,7 @@ pub fn determine_thread_replies(
Reply {
id: tweet.id,
text: decoded_tweet,
content_warning,
attachments: tweet_get_attachments(tweet),
in_reply_to_id: tweet.in_reply_to_status_id.unwrap_or_else(|| {
panic!("Twitter reply ID missing on tweet {}", tweet.id)
Expand Down Expand Up @@ -90,7 +98,14 @@ pub fn determine_thread_replies(
}
}

let fulltext = mastodon_toot_get_text(toot);
let mut fulltext = mastodon_toot_get_text(toot);
let mut content_warning: Option<String> = None;

// add content_warning if present
if toot.spoiler_text.len() > 0 {
fulltext = add_content_warning_to_post_text(&fulltext, toot.spoiler_text.as_str());
content_warning = Some(toot.spoiler_text.clone());
}

// The toot is not on Twitter yet, check if we should post it.
// Check if hashtag filtering is enabled and if the tweet matches.
Expand All @@ -116,6 +131,7 @@ pub fn determine_thread_replies(
.parse::<u64>()
.unwrap_or_else(|_| panic!("Mastodon status ID is not u64: {}", toot.id)),
text: post,
content_warning,
attachments: toot_get_attachments(toot),
in_reply_to_id: in_reply_to_id.parse::<u64>().unwrap_or_else(|_| {
panic!("Mastodon reply ID is not u64: {in_reply_to_id}")
Expand Down Expand Up @@ -157,6 +173,7 @@ fn insert_twitter_replies(
if toot_and_tweet_are_equal(toot, tweet) {
sync_statuses.push(NewStatus {
text: reply.text.clone(),
content_warning: reply.content_warning.clone(),
attachments: reply.attachments.clone(),
replies: Vec::new(),
in_reply_to_id: Some(toot.id.parse().unwrap_or_else(|_| {
Expand Down Expand Up @@ -197,6 +214,7 @@ fn insert_mastodon_replies(
if toot_and_tweet_are_equal(toot, tweet) {
sync_statuses.push(NewStatus {
text: reply.text.clone(),
content_warning: reply.content_warning.clone(),
attachments: reply.attachments.clone(),
replies: Vec::new(),
in_reply_to_id: Some(tweet.id),
Expand All @@ -216,6 +234,7 @@ fn insert_reply_on_status(status: &mut NewStatus, reply: &Reply) -> bool {
if reply.in_reply_to_id == status.original_id {
status.replies.push(NewStatus {
text: reply.text.clone(),
content_warning: None,
attachments: reply.attachments.clone(),
replies: Vec::new(),
in_reply_to_id: None,
Expand Down